Commit Graph
156 Commits
Author SHA1 Message Date
Claude fb59372802 Restore the Katan catalogue entry the engine depends on
CI / Build and check (pull_request) Successful in 13m46s
CI / Deploy to Cloudflare Pages (pull_request) Skipped
The Option 2 migration dropped Gitea's 5c797c8, which added GameCatalog.CATAN
and a Katan entry (available = false: Coming soon on dev, hidden from
production). CatanRules.kt references GameCatalog.CATAN, so the merge did not
compile without it. Taken verbatim from feature/catan-engine, with the
matching CatalogTest assertions.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 20:55:38 +00:00
Claude 653e7a17a3 Merge feature/catan-engine into dev
Brings in the Katan rules engine, board, model and AI (WALA-119), its 51 unit
tests and docs/RULES-catan.md. Only conflict was docs/README.md, which the
GitHub-lineage migration had dropped: kept the engine branch's index and
pointed the CI link (docs/CI.md no longer exists) at the root README.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 20:50:28 +00:00
rootandClaude Sonnet 5.5 33a253d80c CI: keep JVM crash logs and try C1-only JIT on the runner
CI / Build and check (push) Successful in 13m29s
CI / Deploy to Cloudflare Pages (push) Successful in 45s
Runs on the Gitea runner crash intermittently on three JDK vendors. Upload
hs_err/replay logs on failure so the failing frame survives the job
container, and drop C2 (TieredStopAtLevel=1) for the Gradle and Kotlin
daemons to test whether the optimising JIT is the trigger.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YGRgChhwbwWr5yYrHTd9pL
2026-10-14 14:34:10 -06:00
Claude 4ffe817d5f Stage script: use node instead of jq
CI / Build and check (push) Failing after 8m40s
CI / Deploy to Cloudflare Pages (push) Skipped
The Gitea runner's container image has node but no jq, so the first dev deploy
died with exit 127 in stage-pages-site.sh. Same behaviour, tested against mock
servers for: other channel published, 200-with-HTML, 404, and unreachable.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 20:13:21 +00:00
Claude fd352b4d14 Back to Temurin JDK 17 now that api.adoptium.net is reachable from the runner
CI / Build and check (push) Successful in 17m2s
CI / Deploy to Cloudflare Pages (push) Failing after 6s
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 19:43:13 +00:00
Claude 7b5b041c42 Test: wait for the guest's sheet before comparing it
CI / Build and check (push) Failing after 7m34s
CI / Deploy to Cloudflare Pages (push) Skipped
The sheet arrives over the socket just after connected flips, so on a slower
runner the comparison raced it (ScorekeeperSessionTest, line 167). Same
pattern the sibling test already uses.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 17:23:42 +00:00
Claude e63c2cf7ac Use Zulu JDK 17: the Gitea runner cannot reach api.adoptium.net
setup-java's temurin distribution resolves versions through api.adoptium.net,
which fails TLS from the runner (EPROTO) while api.azul.com is reachable.
Any JDK 17 builds the project.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 17:02:54 +00:00
Claude 2314f5f842 Temporary: probe JDK hosts 2026-10-14 17:01:57 +00:00
Claude 2a193f8d5b Temporary: runner egress probe (to be removed) 2026-10-14 17:01:05 +00:00
Claude ad30ab9dea Move CI to Gitea: run-number offset, Cloudflare-only publishing
- plgVersionRunOffset=1000: stamped build number is run number + offset, so
  Gitea's restarted run counter cannot produce a versionCode below an
  installed one (GitHub reached prod 30, dev ~172). Tags and versionName use
  the same number.
- Drop the GitHub release steps; plg.wallacenet.ca is the only channel.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 17:00:01 +00:00
Claude 4c603f6d3d Adopt the GitHub lineage; supersede Gitea-only bootstrap commits
Keeps Gitea's history reachable (also preserved as gitea-pre-github-main)
while the tree stays the GitHub one: package org.prolibertate.games, OTA
from plg.wallacenet.ca. Gitea's package rename and Gitea-releases updater
are intentionally not carried over.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 16:59:38 +00:00
adrianoftyriel 0927660c15 Merge pull request #43 from adrianoftyriel/dev
Dev
2026-10-14 10:42:42 -06:00
adrianoftyriel 5d0f95361f Merge pull request #42 from adrianoftyriel/cf-real-404
Fix Pages carry-over: real 404 and tolerate 200-with-HTML
2026-10-14 10:18:01 -06:00
Claude 6fbe63f787 Fix carry-over on Pages: add 404.html and tolerate 200-with-HTML
Pages serves index.html with 200 for unknown paths when there is no
404.html, so a never-published channel looked published and jq failed on
the HTML. Add a real 404 page and treat a 200 without a latest.json as
absent.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 16:08:28 +00:00
adrianoftyriel 12d20565bd Merge pull request #41 from adrianoftyriel/cf-treat-1014-as-absent
Treat Cloudflare 1014 as 'channel not published yet'
2026-10-14 10:02:26 -06:00
Claude 2df140a3c1 Treat Cloudflare error 1014 as 'channel not published yet'
A push-triggered first deploy cannot set the first_deploy input, and the
domain answers 403/1014 until the Pages project exists. 1014 means nothing
exists to preserve, so handle it like a 404. Other 403s still abort.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 15:53:15 +00:00
adrianoftyriel 0b74b1b2b1 Merge pull request #40 from adrianoftyriel/cf-first-deploy-fix
Fix first Cloudflare deploy: add first_deploy dispatch input
2026-10-14 09:39:58 -06:00
Claude 4369d15cec Add first_deploy dispatch input so the first Cloudflare deploy can bootstrap
The domain answers 403 (error 1014) until the Pages project exists, which
made the carry-over fetch abort the very deploy that would create it.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdMNbY5hLR7FjtfEE712xG
2026-10-14 15:39:03 +00:00
adrianoftyriel 1e9908569d Merge pull request #39 from adrianoftyriel/cloudflare-pages-deploy
Publish dev and prod builds to plg.wallacenet.ca (Cloudflare Pages)
2026-10-14 09:32:31 -06:00
Claude c0fc4fe7d6 Publish dev and prod builds to plg.wallacenet.ca (Cloudflare Pages)
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
2026-10-14 15:20:47 +00:00
adrianoftyrielandPaperclip 3a930f5423 Katan rules engine + AI (WALA-119)
CI / Build and check (pull_request) Failing after 8s
The island game as a pure-Kotlin state machine under game/catan/, wired to
the upstream GameRules / GameAi contract and fully unit-tested on the JVM.

- game/catan/: CatanModel (state, moves, phases), CatanBoard (19-hex
  geometry, seeded island generation with the 6/8 separation rule, shared
  vertex/edge graph), CatanRules (setup, production, the seven and the
  robber, domestic and maritime trade, building, development cards,
  Longest Road, Largest Army, win detection), CatanAi (a heuristic opponent
  that reads only public information).
- GameRules.isLegalMove: a defaulted structural legality check so a game
  with a combinatorial move space (Catan's trade offers) is gated the same
  way as one with an enumerated move list; MatchController uses it.
- GameCatalog.CATAN: the catalogue entry, listed but not yet available
  (descriptor and screen land with the screen task).
- Tests: 51 across CatanBoardTest, CatanRulesTest and CatanAiTest —
  geometry invariants, generation balance, setup, production, the
  seven/robber, both trade types, development-card timing, Longest Road
  edge cases, Largest Army, win detection, and AI games played to a result.
- docs/RULES-catan.md, in the per-game style, with the rule choices and
  the known gaps called out.

Two real engine bugs found and fixed while getting the suite green:
VERTEX_EDGES counted each shared edge once per adjacent hex, so an
interior vertex looked like it had six edges (which also double-counted
segments in the Longest Road search and duplicated setup moves); and the
applyRoll seven-transition was a discarded expression, so a seven never
left AWAIT_ROLL. Three test fixtures rolled from the PLAYING phase and two
assumed a two-seat rotation; corrected to the phases and seat order the
engine actually uses.

Verified: ./gradlew :app:testDebugUnitTest --offline — 679 tests, 0 failures.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 15:13:36 +00:00
adrianoftyrielandPaperclip 4d7cd6182f Rename the installed Android package to ca.wallacenet.prolibertategames
Build and Release APK / Build APK and publish release (push) Failing after 7s
Follows the Gitea repo rename: the applicationId (and its .dev variant)
now matches the repo, the launcher name and the docs, instead of carrying
the abandoned Pilgrims of Katan identity.

The JVM namespace stays org.prolibertate.games, so no source file moves;
only the installed identity changes.

Co-Authored-By: Paperclip <[email protected]>
v1.4.1002-dev
2026-10-14 14:47:13 +00:00
adrianoftyrielandPaperclip b58b4251dc Rename the Gitea repo references to ProLibertateGames
Build and Release APK / Build APK and publish release (push) Canceled after 0s
The repo was renamed from PilgrimsOfKatan to ProLibertateGames on the
Gitea instance. Update every reference so nothing points at the old name:

- OTA updater: the repo constant and the User-Agent
- publish-release.sh: the default repo, the asset name, the release title
- CI/release workflows: the artifact names and the staged APK name
- settings.gradle.kts: the Gradle project name
- Protocol.kt / LanTransport.kt: the mDNS service label and the Wi-Fi lock tag
- docs (README, CLAUDE.md, docs/CI.md) and the update-policy test fixtures

The installed applicationId is left unchanged in this commit.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 14:21:43 +00:00
adrianoftyrielandPaperclip 5c797c8807 Restore the Pro Libertate Games title and splash; list Katan under Board Games
Build and Release APK / Build APK and publish release (push) Canceled after 0s
The bootstrap rebranded the app to Pilgrims of Katan (WALA-118 review);
the decision is to keep the upstream's own identity instead.

- Title and splash: the upstream originals restored exactly — the Wallace
  tartan splash under the "Pro Libertate Games" uncial wordmark
  (SplashScreen.kt, Tartan.kt), the tartan launcher icon, and the
  Theme.ProLibertateGames window theme. The game screens go back to the
  upstream WallaceGold/WallaceRed accents the tartan is woven from.
- Names and colours: launcher names back to "Pro Libertate Games" /
  "Pro Libertate Games DEV"; the channel colours back to the clan red /
  hunting green setts (resValue tartan_field and friends). The installed
  identity stays ca.wallacenet.pilgrimsofkatan (+ .dev) — that is the
  package, not the brand.
- Catalogue: Katan is listed under Board Games (3-4 players, original
  blurb, no third-party marks) with available = false, so dev builds show
  it as "Coming soon" and a production build hides it until its engine
  and screen land.
- Docs: README/CLAUDE.md/docs/CI.md updated to the restored identity;
  CatalogTest's invariant now names Katan as the one deliberate exception
  rather than asserting nothing is unfinished.

Structural bootstrap untouched: applicationId, OTA repointed at the Gitea
releases, CI workflows, version stamping.

Verified: ./gradlew assembleDebug and testDebugUnitTest green — 628 tests,
0 failures.

Co-Authored-By: Paperclip <[email protected]>
2026-10-14 02:15:30 +00:00
adrianoftyrielandPaperclip 05c443a4b5 Publish: verify the release's stored state after attaching the APK
Build and Release APK / Build APK and publish release (push) Canceled after 0s
A live publish on git.wallacenet.ca kept coming back invisible to the updater:
the create accepted "draft": false, and the release was still stored as a
draft once the APK was attached — so the anonymous list the app reads came
back empty even though every publish step reported success.

The script now attaches first, then reads the release back and PATCHes the
state if it did not stick, failing loudly if the correction is refused. A
draft is not a channel a preview build can silently become.

Also lists all 22 catalogue games in the README's table; seven were missing,
which read as "the app has 15 games".

Verified against the live instance: anonymous list shows
v1.4.1001-dev as a published prerelease with a tag-named asset, the
production /releases/latest correctly returns 404 while only a prerelease
exists, and an anonymous download through the URL the app builds matches the
built APK byte for byte.

Co-Authored-By: Paperclip <[email protected]>
2026-10-13 22:27:33 +00:00
adrianoftyrielandPaperclip a827a38ae6 OTA: name release assets after their tag, download from one host
Build and Release APK / Build APK and publish release (push) Canceled after 0s
Two things the first live release check on git.wallacenet.ca showed:

- The publish script uploaded the APK under whatever the build called it
  (app-debug.apk), which is the name the updater only finds by its fallback
  rule. The script now names the asset after its tag, so the primary rule —
  "the asset whose name contains the tag" — is the one that fires.
- The instance answers for two host names and reports asset URLs on the one
  it treats as canonical, so the app would have fetched its release list from
  git.wallacenet.ca and its APK from elsewhere. onReportedHost() now keeps the
  path the server chose and puts the download on the host the app already
  trusts, tested in UpdatePolicyTest.

Co-Authored-By: Paperclip <[email protected]>
v1.4.1001-dev
2026-10-13 22:13:09 +00:00
adrianoftyrielandPaperclip fad4778ad8 Bootstrap Pilgrims of Katan: rebrand, Gitea CI and OTA
Build and Release APK / Build APK and publish release (push) Canceled after 0s
Import of ProLibertateGames (upstream main, GPL-3.0) rebranded for
Wallacenet and repointed at git.wallacenet.ca.

- Identity: applicationId ca.wallacenet.pilgrimsofkatan (+ .dev for the dev
  channel), launcher names "Pilgrims of Katan" / "Pilgrims of Katan DEV",
  neutral hexagon branding replacing the Wallace tartan. Uncial Antiqua keeps
  its OFL licence, and the GPL-3.0 LICENSE plus upstream attribution stay.
- OTA: Updater.kt now reads this repo's own Gitea releases
  (/api/v1/repos/devadmin/PilgrimsOfKatan/releases...) and BuildConfig.
  OTA_ENABLED is on; Gitea answers the same JSON shape the code already parsed.
- Build numbers: versionCode = CI run number + plgVersionRunOffset (1000),
  with the tag and versionName derived from the same figure. The offset is
  permanent, because Gitea Actions restarts run numbers at 1 and a reset would
  otherwise make Android reject the next build as a downgrade.
- CI: ci.yml publishes a dev prerelease from dev, release.yml a production
  release from main, both through scripts/publish-release.sh (Gitea REST API,
  runnable by hand until an Actions runner is registered).
- Docs: docs/CI.md records the channel, numbering and runner decisions;
  docs/README.md seeds the per-game RULES-* conventions; CLAUDE.md carries the
  agent ground rules.

Verified: ./gradlew assembleDebug and testDebugUnitTest green on JDK 17 /
compileSdk 34, both variants stamped, installing side by side under the new
identity.

Co-Authored-By: Paperclip <[email protected]>
2026-10-13 22:03:27 +00:00
adrianoftyriel d27c2db894 Merge pull request #38 from adrianoftyriel/dev
Release: multi-device scorekeeper sharing and tally scoring
2026-10-12 22:04:30 -06:00
adrianoftyriel 4037f75136 Merge pull request #37 from adrianoftyriel/feature/tally-number-box
CI / Build and check (push) Failing after 1m40s
Tally scoring: inline number box instead of dialog
2026-10-12 21:24:56 -06:00
devadminandClaude Sonnet 5.5 9df9fcd556 Tally: replace the custom-amount dialog with an inline number box
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-13 03:24:33 +00:00
adrianoftyriel 4ae16c7ca4 Merge pull request #36 from adrianoftyriel/feature/tally-scoring
Scorekeeper: round-based or tally scoring
2026-10-12 17:15:56 -06:00
devadminandClaude Sonnet 5.5 5c0a44c11d Scorekeeper: choose round-based or tally scoring when starting a game
Tally sheets let any player score at any moment (+1, -1 or a custom amount)
with a log of entries that can be taken back. Entries are stored as one-player
rows so totals, undo and sharing work unchanged.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-12 21:43:37 +00:00
adrianoftyriel 6030e62bbe Merge pull request #35 from adrianoftyriel/fix/scorekeeper-join-diagnostics
Scorekeeper sharing: join from any sheet, show waiting requests, diagnostics
2026-10-09 21:29:09 -06:00
James WallaceandClaude Sonnet 5.5 d9fdb598ac Scorekeeper sharing: join from any sheet, show waiting requests, diagnostics
A guest was timing out with nothing ever shown on the host. Without a
device to look at, this makes the next test say where it stops, and fixes
what was found on the way.

- The host's Sharing panel shows links received and join requests read, which
  tells "nobody reached this phone" from "reached it and not understood".
- The guest says when its link is up and it is waiting for an answer.
- A request waiting shows on the host's top bar ("Sharing . 1 waiting"), and
  the Sharing panel steps aside while one is up so two dialogs never stack.
- Join is a top-bar action on a sheet already under way; it was only offered
  on an empty one.
- LanTransport: a host being cleaned up no longer unregisters the
  advertisement or clears the address of a newer host started in its place.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CBG7URddsUZjQ6KE1Kqf6G
2026-10-10 02:56:36 +00:00
adrianoftyriel 3e277754e9 Merge pull request #34 from adrianoftyriel/feature/multidevice-scorekeeper
Scorekeeper: share a sheet between devices
2026-10-09 19:55:50 -06:00
James WallaceandClaude Sonnet 5.5 89b19a894a Scorekeeper: share a sheet between devices
A host can share its score sheet and others nearby can join it, found the
same way games are, or by typed address over a hotspot.

- A joining guest is not let in on its own say-so. The host is prompted with
  the guest's name and answers Allow or Deny. A device already let in that
  drops out (locked phone) is let straight back; the host can remove anyone.
- The host's sheet is authoritative. What travels is what was done (SheetOp:
  add round, rename, move column, ...), not whole sheets, so changes made at
  the same moment all land. A guest's own changes show at once and are laid
  over the host's sheet until confirmed (SheetReplica).
- Sync asks the host for the sheet as it stands, and runs on its own when
  the app returns to the front, as it does for a game table.
- Hello carries a purpose and hosts advertise one, so a game join screen does
  not list score sheets and a game guest typing a sheet's address is told so.
- The Scorekeeper screen now expresses every edit as a SheetOp, so one grid
  serves a local sheet, a hosted one and a joined one.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CBG7URddsUZjQ6KE1Kqf6G
2026-10-10 01:27:40 +00:00
adrianoftyriel e6ce1aebd5 Merge pull request #33 from adrianoftyriel/dev
Release 1.4
2026-10-09 14:13:52 -06:00
adrianoftyriel 16f9321ca3 Merge pull request #32 from adrianoftyriel/fix/ci-setup-android-v4
CI: bump setup-android to v4
2026-10-09 14:02:08 -06:00
James WallaceandClaude Sonnet 5.5 c89c8d15c7 CI: bump setup-android to v4
v3 runs sdkmanager with the tools package, which Google's repository no
longer has, so the Set up Android SDK step failed before any build.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CBG7URddsUZjQ6KE1Kqf6G
2026-10-09 19:34:12 +00:00
James Wallace f72405f28f Merge branch 'claude/card-game-physics-animations-5b6bfi' into dev
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

# Conflicts:
#	app/src/main/java/org/prolibertate/games/net/LanTransport.kt
2026-10-09 15:47:47 +00:00
James WallaceandClaude Sonnet 5.5 45bc16594e Bump version series to 1.4
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-02 21:23:38 +00:00
James Wallace 6913ee2fe9 Merge branch 'fix/scorekeeper-zero-and-cell-edit' into dev 2026-10-02 21:19:59 +00:00
James WallaceandClaude Sonnet 5.5 b98d464954 Scorekeeper: show an entered 0, tap a past score to edit it
Zeros are now stored so a typed 0 shows as 0 while a blank cell stays blank.
Tapping a score in an earlier round opens that round for correction with the
cursor in the tapped cell, instead of needing the row number first.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-02 21:19:58 +00:00
adrianoftyriel 26ae3fecaa Merge pull request #31: get back into a game a locked phone dropped out of
Get back into a game a locked phone dropped out of
2026-08-17 09:45:40 -06:00
Claude 5d1d2e204e Merge remote-tracking branch 'origin/dev' into claude/locked-phone-turn-issue-x9pvbv 2026-08-17 15:33:41 +00:00
Claude f0c8895747 Get back into a game a locked phone dropped out of
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
2026-08-16 00:28:05 +00:00
Claude deb4acf96b Give the hotspot's owner a route to its guests too
Binding the join socket to the Wi-Fi network covers a phone that has
joined a hotspot. It does nothing for the phone that is sharing one:
that phone has no Wi-Fi *client* network, so there was nothing to bind
to and the code gave up — leaving the socket on the default network,
which is mobile data, and a connection to a guest going out over
cellular to die.

ConnectivityManager does not offer the tethering interface as a Network,
so there is no network to name. Binding this end of the socket to one of
our own addresses on the guest's subnet picks the route instead, which
is the only lever available. The subnet test assumes a /24, which every
Android hotspot hands out, and getting it wrong costs a bind that would
not have helped anyway.

This is the direction that was left broken: the owner of the hotspot
trying to join a game hosted by one of its guests.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0132V9gE6DF9itz2z3Hn7BK9
2026-08-16 00:25:54 +00:00
Claude f8f02dd93e Make LAN play work over a phone's hotspot
Two phones on one phone's hotspot could not find or reach each other.
Nothing was wrong with the Wi-Fi. There were three faults, and the first
one is the whole bug.

A hotspot has no internet behind it, and Android takes that literally:
the Wi-Fi network fails its connectivity check, stays unvalidated, and
mobile data remains the *default* network. Every socket opened without
naming a network is therefore routed out over cellular — where
192.168.43.1 means nothing. The join socket is now bound to the Wi-Fi
network before it connects, and the search for that network deliberately
does not ask whether it has internet, because asking is what breaks it.

Second, the manifest has asked for CHANGE_WIFI_MULTICAST_STATE since the
beginning, with a comment saying discovery needs it, and nothing ever
took the lock. Without it the radio drops multicast frames not addressed
to this device before the system sees them, and mDNS is multicast. Both
advertising and discovering now hold it.

Third, and not fixable: a phone sharing its own hotspot does not
reliably advertise over the tethering interface, whatever the other end
does. So the host now shows where it is and the joiner can type it in.
That path is offered always rather than kept back for when discovery has
failed, because discovery here does not fail loudly — it just never
finds anything, and a screen saying only "no games found" leaves nowhere
to go. Hosts take a known port when they can get one, so what gets typed
is an address and not an address and five more digits.

The lobby scrolls instead of filling. Its lazy lists took the whole
screen for a handful of rows and pushed the Start button off the bottom;
they would have done the same to the address box.

Parsing what somebody types off another phone's screen is the one pure
piece of this and is tested, including that what a host displays is
exactly what the other end accepts. The rest needs two phones and a
hotspot, which CI does not have.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0132V9gE6DF9itz2z3Hn7BK9
2026-08-15 23:52:39 +00:00
adrianoftyriel d7a98c31c8 Merge pull request #30: play onto the pile, and keep the tricks you win
Play onto the pile, and keep the tricks you win
2026-08-13 16:17:05 -06:00
Claude cba66906bb Keep won tricks in front of the player who took them
A trick slid off towards its winner and stopped existing. Where it went
was the only record that it had been won at all, and by the third trick
of a hand there was nothing on the table to say who was sitting on what
— which on a real table is the most-glanced-at thing in the game, and
the whole reason cards are gathered rather than binned.

Each player now has a face-down pile in front of them: one card back per
trick, because a trick is squared into a single bundle and put down, and
from above that is what you see. The piles sit beside each player's own
card, one place clockwise along their edge, which is both where a player
would actually put them and the one spot on each edge nothing else was
using. Wizard seats three to six so its piles go half a seat clockwise
round the same ring the cards are played on.

The count follows the cards, not the rules. A trick is counted the
instant its last card is played, which is a beat before the gather and
two before the cards arrive; reading the state directly grew the
winner's pile while the trick was still lying in the middle of the
table, and the cards then slid over and landed on a pile that had
already counted them. rememberArrivedTricks holds the count back until
the sweep has actually run.

The sweep changes to suit its new destination. A gathered card is
squared down towards the size of the bundle it is joining, and stays
whole until the last stretch instead of fading the whole way over: it
now vanishes at the same moment the pile grows by one, and that swap is
only invisible if there was something there to swap.

Kaiser needed a per-seat trick count, which it had never had — it scores
in points, so no rule wanted one. It is defaulted for the wire and
carries a test, because a field no rule depends on is a field that can
quietly stop being maintained. Hearts already keeps the cards themselves
and counts them by the deal.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0132V9gE6DF9itz2z3Hn7BK9
2026-08-13 22:10:04 +00:00