<feed xmlns='http://www.w3.org/2005/Atom'>
<title>typerpunk/TODO-postgres.md, branch main</title>
<subtitle>Rust + WASM typing game.
</subtitle>
<id>https://srdusr.com/git/typerpunk/atom?h=main</id>
<link rel='self' href='https://srdusr.com/git/typerpunk/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/typerpunk/'/>
<updated>2025-12-07T18:38:00+00:00</updated>
<entry>
<title>Move the server to PostgreSQL, harden the lyrics proxy, add a hacking mode</title>
<updated>2025-12-07T18:38:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-12-07T18:38:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/typerpunk/commit/?id=5b1ea38522dbf6bf60db5a2270463de0c12d9de3'/>
<id>urn:sha1:5b1ea38522dbf6bf60db5a2270463de0c12d9de3</id>
<content type='text'>
PostgreSQL
- sqlx switched from the sqlite feature to postgres; the server now runs on
  Postgres 18 and the SQLite file is gone.
- 95 placeholders renumbered from ? to $N.
- REAL widened to DOUBLE PRECISION: Postgres REAL is float4 and will not
  decode into the f64 the code reads.
- flagged and is_bot are real BOOLEANs rather than 0/1 integers, with the
  decode side reading bool.
- The leaderboard's derived table gained the alias Postgres requires, its
  flag comparisons became boolean predicates, and INSERT OR IGNORE became
  ON CONFLICT DO NOTHING.
- u32 binds cast to i64; Postgres has no unsigned integer types.
- Integration tests run against a real database - Postgres has no in-memory
  mode - each in a throwaway schema, with search_path set per connection
  because it is session state and the pool opens more than one.
- Timestamps stay TEXT for now and LISTEN/NOTIFY is still unused; both are
  recorded in TODO-postgres.md rather than left implied.

Custom text and lyrics, checked rather than assumed
- Custom files never reach the server: they are read in the browser through
  the File API, so there is no upload, no path handling and no remote file
  inclusion to have. Verified by driving a hostile file - markup in the body
  and in the filename - all the way onto the typing screen: it renders as
  literal characters, no nodes are created, nothing executes, and the
  filename is escaped in the attribution too.
- That test found a real regression: picking Custom from the new mode picker
  selected it without ever starting it, so the mode was unstartable.
- /api/lyrics fixes its upstream host, so it cannot be pointed elsewhere, but
  it was an unbounded relay: now rate limited per IP, with length caps on
  artist and track and a ceiling on the response body it will read.

Hacking mode
- 22 single-line drills across recon, web, memory safety, exploit
  development, crypto, post-exploitation and defence, each syntax
  highlighted and each explaining what the line actually does.

All 19 modes verified to start, render and be typable.
</content>
</entry>
<entry>
<title>Add multiplayer bots, typing languages, and rework the UI layout</title>
<updated>2025-09-11T19:53:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2025-09-11T19:53:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/typerpunk/commit/?id=a726d9f5fb56e1fd7983c5ac806d408ad78daa86'/>
<id>urn:sha1:a726d9f5fb56e1fd7983c5ac806d408ad78daa86</id>
<content type='text'>
Multiplayer
- Quick match: POST /api/multiplayer/quickmatch returns whichever room is
  still filling, or opens one. Players never see a room code; joining by
  code stays for racing specific people.
- Bots fill quick-match rooms after a short wait so a new game is never an
  empty lobby. They only ever join quick-match rooms, never a room opened
  by code. One or two per room, drawn from separate ~40 and ~80 WPM tiers so
  two bots are never near each other's pace, and they stall to correct
  mistakes rather than typing a clean straight line.
- Live player count via GET /api/multiplayer/online, shown on the
  Multiplayer control and under the main menu's Multiplayer button.
- Per-racer colours: you are the theme accent, opponents take distinct hues
  that stay the same from lobby to race.
- The countdown no longer holds the room lock for its full three seconds,
  which is what reset clients mid-countdown.

Typing languages
- 16 languages for the generated-word modes, each with its own
  high-frequency vocabulary rather than a translation of the English list.
- Picker in the top-right rail; non-English uses its own list at every
  difficulty tier instead of falling back to English words.

Fix UTF-8 accuracy in the game core
- update_game_state mixed byte and character counts: total_characters_typed
  accumulated byte-length deltas while total_correct_characters compared a
  char index against that byte count. Equal on ASCII, so it went unnoticed;
  a correctly typed Spanish passage scored 6%. The old byte slicing would
  also have panicked if an index landed inside a multi-byte character.
  Rewritten char-based, with regression tests.

Programming mode
- Replaced prose about programming with real code: 26 syntax-highlighted
  snippets across JavaScript, Python, Rust, C/Go/Java and shell. Single-line
  by necessity, since the typing input is a single-line field.

Layout and readability
- One icon rail arrangement on every screen: Settings/Store under the
  wordmark, Language/Theme/Friends/Account top-right, Stats/Leaderboard/
  Multiplayer bottom-right.
- Main menu: mode picker moved out of the Single Player button, which it was
  notching a divider through and pushing the label off-centre.
- Escape returns to the menu, closing any open popover first, and confirms
  before abandoning a live race.
- Split --text-color and --sub-color per theme; they shared one value that
  measured 3.65:1 against the background, below the 4.5:1 body-text floor.
- Semantic colours used in exactly one place each: gold for a personal best,
  amber for the race countdown and the mobile-result badge.
- Passage now sits in the same place on the typing and end screens, and its
  column is a whole number of characters wide so wrapping cannot leave a
  permanent gap on the right.
- End screen: keystrokes and a correct/wrong/extra/missed split, attribution
  carried over from the typing screen, and a graph with a separate error
  axis, axis titles including seconds, and smoothed lines.
</content>
</entry>
</feed>
