Both workflows gain a deploy-pages job that stages their channel into one
Pages site, carrying the other channel over from the live site. The in-app
updater now reads /prod|/dev/latest.json from plg.wallacenet.ca instead of
the GitHub API. GitHub releases are still published for one transition
deployment so existing installs can fetch the new updater.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
A phone locked when its turn came round could not play once it was
unlocked. The game was never wrong — the host still had the position and
the seat was still the player's — but the only route to it was gone, and
nothing rebuilt it. The board came back with the state it had before the
turn arrived, and every tap on it went nowhere.
A locked phone puts its Wi-Fi radio into power save, and a link carrying
nothing between one turn and the next is exactly what that drops. Four
things were missing, and all four are needed:
The radio is held awake for as long as there is a game on, which stops
most of these drops happening at all. FULL_HIGH_PERF rather than the
LOW_LATENCY its deprecation notice points at: that one only applies with
the screen on and the app in front, which is the case that worked.
A link with nothing on it is pinged, and answered. TCP does not report a
link that died this way — the read never returns and the next write goes
into a socket that leads nowhere — so without traffic of its own the app
could not tell a player who was thinking from one who was gone. Both ends
have to speak it or a quiet link would be dropped as dead, hence the
protocol bump; the lobby already turns a mismatch away with "update both
devices".
A write that fails now reports the link as broken instead of closing it as
though this end had meant to. That silence was the reason a dead board
answered nothing and said nothing about why.
And a guest that loses its link dials the host again — which is still
listening, on the same port, for the whole match — and is put back in the
seat it never actually left, by the device id it is already known by. The
table then resyncs, which is what fills the screen back in. Coming back
to the front asks for the same thing, since a client draws nothing but
what it was sent and anything pushed while the link was down was pushed
at nothing.
Sends are serialised while I am here: a message is a line, a write is
three calls, and the heartbeat writes on its own schedule.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GarwvAPvWHELNspro4foFY
The same sheet, drawn as the paper it replaces: a column per player with
their name at the head, a row per round running down the page, and the
running total ruled off at the foot.
The bottom row is always the one being filled in. Finishing it writes it
down and opens a fresh empty row above the totals, so the place to type
is always in the same spot. The head and the totals sit outside the
scrolling rounds and share one horizontal scroll state with them, which
is what keeps a column and the name over it from ever coming apart —
every row is built by the same composable and is therefore exactly the
same width, which is what lets them share it.
Reordering moves to where a spreadsheet puts it: drag a name sideways
and the whole column travels with it. Tapping a name renames it or
removes that player, tapping a round number down the left edge opens
that row for correction, and a + column at the right adds a player. The
mode toggle and the round dialog are gone with them.
Points still go in as typed text until the row is finished, so a
half-typed minus is not a number yet. The ± button flips the last number
typed: not every keyboard offers a minus on its number pad, and taking
points away has to work on all of them.
adjustResize is now stated in the manifest rather than left to the
system to resolve. This screen types into the bottom row with the totals
and the finish button below it, and they have to stay above the keyboard
rather than being panned off the screen.
The model is untouched: ScoreSheet and its tests did not need to change
for any of this, which is the point of it having no Android in it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019Pr6B8xDDtJJcb6WJMw7Ud
Multiplayer now runs over Wi-Fi (LAN) only.
- Delete BluetoothTransport and drop it from LobbyController's transport
list, leaving LanTransport as the sole transport.
- Drop TransportKind.BLUETOOTH and the now-unused RFCOMM_UUID constant.
- Remove the BLUETOOTH/BLUETOOTH_ADMIN/BLUETOOTH_CONNECT permissions and
the android.hardware.bluetooth feature from the manifest, along with
the runtime permission request in MainActivity and the
requestBluetoothPermissions hook on AppEnv.
- Update lobby copy so an empty transport list and an empty host list
talk about Wi-Fi rather than pairing, and refresh the comments and
README sections that described two transports.
The transport abstraction itself is kept: everything after the handshake
was already transport independent, so adding another one later needs no
changes above LanTransport.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YEsqYAMJKUJoM7UAavm169
Lint flagged ACCESS_FINE_LOCATION declared without ACCESS_COARSE_LOCATION,
which Android 12 requires to be requested together.
Rather than add the missing one, drop location entirely: the Bluetooth
transport only talks to already-paired devices, enumerating bonded
devices and opening an RFCOMM socket. Neither is a scan, so no location
access is needed. BLUETOOTH_SCAN and BLUETOOTH_ADVERTISE go for the same
reason — nothing was using them.
Discovery of unpaired devices would bring all of these back; the manifest
notes what that would require.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN
First vertical slice: the whole shell plus two games playable end to end,
so every layer is exercised before the other nine games are built on it.
Games
- Euchre: bowers, ordering up, dealer discard, going alone, stick the
dealer, and full scoring. Rule variants (deck size, points to win,
stick the dealer, going alone) are toggles in game setup.
- Sequence: two-eyed and one-eyed jacks, free corners, locked sequences,
the shared-chip limit, and dead-card exchange. Teams, players per team
and the sequence target are toggles.
- The board layout in SequenceBoard is generated, not the commercial
board — it holds every structural property the rules need, but does not
match a physical board. Documented in docs/RULES-sequence.md.
Multiplayer
- One transport abstraction over newline-delimited JSON, implemented
twice: LAN via NSD discovery and TCP, Bluetooth via Classic RFCOMM.
A lobby advertises on both at once, so a table can mix them.
- Host-authoritative: clients send move intents, the host re-validates
every one against the rules, and each client receives a state redacted
for its own seat so hidden information never crosses the wire.
- Seats start as AI and are handed to people as they join, which is what
makes any mix of humans and AI work without a separate code path.
Shell
- Main menu lists all eleven games; the nine without engines are marked
coming soon and are driven entirely by GameCatalog.
- Settings: sound, animation speed (one multiplier drives card movement
and AI thinking time), and OTA updates from the latest GitHub release.
- Layouts measure available space rather than assuming an orientation.
The game package is free of Android types, so the rules engines and AI
run under plain JUnit; 43 tests cover ranking, following suit, scoring,
sequence detection, redaction, wire round-trips and full random and AI
playthroughs.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015qjfMiPUDshdKEUa6vxqzN