<feed xmlns='http://www.w3.org/2005/Atom'>
<title>typerpunk/web/tests/helpers.py, 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>2026-01-13T19:14:00+00:00</updated>
<entry>
<title>Fix the store showing every item as free, and six layout defects</title>
<updated>2026-01-13T19:14:00+00:00</updated>
<author>
<name>srdusr</name>
<email>99972264+srdusr@users.noreply.github.com</email>
</author>
<published>2026-01-13T19:14:00+00:00</published>
<link rel='alternate' type='text/html' href='https://srdusr.com/git/typerpunk/commit/?id=695bf0df2ec48e67d716dad25711cdc2e96bf7f4'/>
<id>urn:sha1:695bf0df2ec48e67d716dad25711cdc2e96bf7f4</id>
<content type='text'>
The catalogue served every price as $0.00. The Cosmetic struct typed
price_cents as i64 against an INTEGER column, so sqlx refused the decode and
unwrap_or_default turned the failure into a zero. Nothing was logged. The
whole store read as free while the database held the real prices.

Decode errors are now returned rather than defaulted away, and create_session
refuses any amount at or below zero, so a price that fails to decode cannot
become a session that grants an item without charging.

Layout, all measured in a real browser at 1280, 820 and 390 pixels:

- The close button sat underneath the fixed top rail. The rail reaches into
  the content column on any viewport under about 1350px, so the overlap was
  there for nearly every visitor. The button starts below the rail now.
- The wordmark and the top rail overlapped by 75px at 390px. Both are fixed
  to the top of the viewport and neither knew about the other. Both give
  ground on narrow screens, and Sign Up drops out of the rail because Sign In
  reaches the same screen.
- Store rows were 93px tall for one line of content, because .menu-button
  carries a vertical margin meant for a stacked menu. Twenty six items came
  to 2400px of scrolling. Rows are 47px and the page is 2467px rather than
  3784px.
- .stats-screen centres its children, so any child without a declared width
  shrink-wraps. That left the sign-in box at 415px, the bundle grid at 488px
  (which collapsed it to a single column) and the leaderboard table at 498px,
  all inside a 636px column. Every screen was swept for the same defect.
  .account-panel is the one narrow child that is deliberate: it declares
  max-width 360px because a sign-in form should not be 636px wide.
- Leaderboard rows had a hard 461px minimum from fixed column widths, so at
  390px the row ran from x=-36 to x=426 and the date column was cut off the
  side of the screen. The date is hidden on narrow viewports.
- The profile links in the leaderboard were 21px tall, under the 24x24
  minimum target size, and they are the only route to a player's profile.

Sprites never showed as equipped: the store compared the equipped caret and
flair but not the sprite.

The store test bought items by clicking Buy, which used to grant them for
nothing. It now asserts that Buy does not grant, then grants the items the
way a signature-verified webhook would, and goes on to check that an equipped
caret colour reaches the typing screen.

All 21 screen and viewport combinations are clean for overlap, overflow,
clipped content and target size. 3/3 browser tests pass.
</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>
