collin/mahjong · bf80fd25
Say it from where the player is sitting
Collin Richards · 2026-08-25 08:02 UTC · bf80fd25d16d8a9fa574610a0d7110811e9baa7d · parent 0ab65f87 · browse files
modifiedREADME.md+64 −1
| ⋯ 147 unchanged lines | |||
| 148 | 148 | 五萬, 東風. A flower says 補花 and then which one. A new call cuts off one | |
| 149 | 149 | still being spoken; at table pace the newest is the only one that matters. | |
| 150 | 150 | See [the voice pack](#the-voice-pack) for where the audio comes from. | |
| 151 | + | - **方位音 (sound by seat)** — a third switch, on by default. Every noise comes | |
| 152 | + | out of the edge its seat is sitting at: 上家 to your left, 下家 to your right, | |
| 153 | + | and 對家 across the table, where it is quieter and has the top taken off it — | |
| 154 | + | a metre of air and three people's arms do not carry 12 kHz. So you can tell | |
| 155 | + | whose 碰 that was without looking up, which is the whole point of hearing it. | |
| 156 | + | Tiles out in the middle are placed properly rather than by edge: the physics | |
| 157 | + | knows to the pixel where a tile landed, and that is where the knock comes | |
| 158 | + | from. Online the table is turned so your own seat is at the bottom, and the | |
| 159 | + | sound turns with it. See [audio, by seat](#audio-by-seat). | |
| 151 | 160 | - **Settings** — 設定 from the lobby, or from the game-over panel: player names, | |
| 152 | 161 | 底 / 台 / starting chips, the house-rule switches below, and sound. Kept in | |
| 153 | 162 | `localStorage` separately from the save, so they carry over to the next game. | |
| ⋯ 31 unchanged lines | |||
| 185 | 194 | tag on the nameplate to show it again for a player who lost theirs. A phone | |
| 186 | 195 | that leaves hands the seat back to the laptop. Seats never lock: a second | |
| 187 | 196 | phone scanning the same QR joins the same hand — two people playing one | |
| 188 | - | hand, whoever acts first acts. | |
| 197 | + | hand, whoever acts first acts. And with sound on, the phone in your hand is | |
| 198 | + | what says your calls — see [audio, by seat](#audio-by-seat). | |
| 189 | 199 | ||
| 190 | 200 | Design notes, in the order they bit: | |
| 191 | 201 | ||
| ⋯ 220 unchanged lines | |||
| 412 | 422 | House rules vary a lot; the tai table and payments are plain data/functions in | |
| 413 | 423 | `tai.ts` and `types.ts` if yours differ. | |
| 414 | 424 | ||
| 425 | + | ## Audio, by seat | |
| 426 | + | ||
| 427 | + | A sound at a mahjong table comes from somewhere. You know whose 碰 that was | |
| 428 | + | because it came from your left, and you know a tile has been thrown hard at the | |
| 429 | + | far corner without watching it go. Both of those are cheap to keep, and both | |
| 430 | + | are gone the moment everything comes out of the middle of one screen. | |
| 431 | + | ||
| 432 | + | **Every cue is a seat's.** `SoundCue` carries a `seat` alongside the kind and | |
| 433 | + | the tile — the seat that threw, called, or won — and leaves it off the things | |
| 434 | + | that belong to the table rather than to anybody: the deal, a 流局, an undo. The | |
| 435 | + | engine says whose only because a sound has to come from somewhere; what that | |
| 436 | + | means in the room is entirely the UI's business, as it always was. | |
| 437 | + | ||
| 438 | + | **A seat becomes a place.** `App.tsx` turns a seat into a *position round this | |
| 439 | + | screen* — 0 the near edge, 1 the right, 2 across, 3 the left — because that is | |
| 440 | + | what the ear wants and the seat number is not: online the table is turned so | |
| 441 | + | you are always at the bottom, and the sound has to turn with it. | |
| 442 | + | `game/sound.ts` gives each position a bus of its own: a pan, a gain, and a | |
| 443 | + | lowpass that opens all the way for the near edge and closes to 5 kHz for the | |
| 444 | + | far one. Distance is two things at once — quieter, and with the top taken off | |
| 445 | + | — because between you and 對家 there is a metre of air, a wall of tiles, and | |
| 446 | + | three people's arms, none of which carry treble. Tiles out in the middle skip | |
| 447 | + | the edges entirely and are placed continuously, panned by where on the felt | |
| 448 | + | they actually landed; the physics knows that to the pixel. Buses are built once | |
| 449 | + | per spot and quantised, so a pool full of skidding tiles reuses a handful | |
| 450 | + | rather than building one per contact. Old Safari has no `StereoPannerNode`; it | |
| 451 | + | plays where it always did. | |
| 452 | + | ||
| 453 | + | **At a party table, the words come out of the player.** Five devices in one | |
| 454 | + | room, and the split the sound should take is the one the room already has: the | |
| 455 | + | table makes the noise, because the tiles are on the table, and the phone in | |
| 456 | + | somebody's hand says their calls, because the calls are theirs. So the party | |
| 457 | + | screen drops the *voice* for a seat while keeping the clack, and the phone | |
| 458 | + | holding that seat says 碰 and names its own discards and makes no table noise | |
| 459 | + | at all. | |
| 460 | + | ||
| 461 | + | That only works if the phone really is going to say it, so it is asked rather | |
| 462 | + | than assumed. `sound.voiceLive()` is the honest answer — sound on, 報牌 on, the | |
| 463 | + | audio hardware awake, the pack decoded — and every device reports it to the | |
| 464 | + | host whenever it changes (`EV_VOICE`). The host works out, seat by seat, which | |
| 465 | + | hands have a device that will speak for them and publishes that as `voices` in | |
| 466 | + | room state; a nameplate wearing 🔊 next to its 📱 is a seat the table has gone | |
| 467 | + | quiet for on purpose. Mute the phone, put it to sleep, hand the seat back — | |
| 468 | + | the flag drops and the table takes the calls back mid-hand. The host's own | |
| 469 | + | device never counts, even when it holds seats: it is the table, and a table | |
| 470 | + | that fell silent on the grounds that it was about to speak would say nothing at | |
| 471 | + | all. | |
| 472 | + | ||
| 473 | + | A browser will not start audio before the page has been touched, and a phone | |
| 474 | + | that arrived by QR may never touch anything that asks for it by name — so the | |
| 475 | + | first touch anywhere unlocks it, which is also the moment it can start | |
| 476 | + | claiming the job. | |
| 477 | + | ||
| 415 | 478 | ## The voice pack | |
| 416 | 479 | ||
| 417 | 480 | A mahjong table only ever says about fifty things — 42 tile names and a handful | |
| ⋯ 301 unchanged lines | |||