Gettysburg is a browser-native, two-player adaptation of a turn-based hex-grid battle game. The project is intended to preserve the tabletop flow while adding modern multiplayer, saved games, server-authoritative state, and a clean user interface that can be self-hosted on the dedicated Gettysburg VPS.
The local project workspace contains supplied board, rules, and order-of-battle
references that are deliberately ignored by Git. The repository contains only
the approved implementation plan and rights-safe project work. The current
application candidate enables mandatory Scenario Five for new games; complete
phase/release acceptance remains tracked in ROADMAP.md and TASKS.md.
The latest reviewed deployment is d018e92 (2026-10-07). Road/rail movement
and eligible reinforcement entry cost 1 for new games and the explicitly
transitioned testing game. Earlier replay/costs and other saves are preserved.
Game creation pauses at 90% host CPU or RAM; other abuse protections remain.
Encrypted pre/post backups, exact-image scanning, transition verification, and
public two-client smoke passed. See TASKS.md for evidence and rollback limits.
Reviewed PR #73 source 2990d8eb44f5b98b1fa96930234fb818382e7bc3 was deployed
on 2026-10-06 after independent review, exact-head CI, vulnerability scanning,
and verified encrypted backups. Per-game persistence replaces whole-service
history rewriting. The owner-authorized purge removed 63 old development games;
database volumes, credentials, audit history, and recovery backups were retained.
Public desktop/tablet full 24-turn games now pass with unchanged bounds, reload,
and exact replay. Move synchronization was observed at 248-260 ms instead of
5.36-5.87 seconds. Public readiness, two-client HTTPS/WebSocket checks, and
restart/resume pass; gameplay is available for owner testing, not final release.
Owner testing exposed a separate proxy source-attribution defect after the
acceptance harness exhausted the creation budget. A controlled restart cleared
that window without changing limits; proxy isolation remains an open repair.
Phase 2 historical operational closeout remains
complete. The deployed candidate uses
gettysburg-mandatory-v5 / gettysburg-mandatory-board-v2 for new games, including
weighted movement, continuous stack activation, reinforcement costs, connected
terrain defense, retreat/advance, and mandatory night withdrawal. Saved versions
are not silently reinterpreted. Dependency advisories were patched separately.
Push remains disabled pending signing-key/provider setup. Retained-version replay,
and Phase 4 release gates remain open. The owner accepted the board and rules
on 2026-10-06; that approval does not close the remaining technical gates.
See TASKS.md for dated evidence; this is a development deployment, not a claim
of production readiness.
PLAN.md- product direction, decisions, delivery strategy, and gatesSPEC.md- required behavior, architecture, security, and acceptance criteriaROADMAP.md- ordered implementation phases and exit criteriaTASKS.md- current reviewable work and validation statusdocs/operations/RELEASE_ACCEPTANCE.md- remaining owner/device/VPS checksdocs/references/SOURCE_ASSETS.md- local-only source inventory and missing inputsdocs/references/TERRAIN_ADJUSTMENTS.md- per-hex terrain/modifier owner-review worksheetdocs/operations/VPS.md- verified deployment target and operating model
- React, TypeScript, and Vite for the browser client
- SVG for the first board renderer
- Colyseus for authoritative multiplayer rooms
- shared pure TypeScript rules and data packages
- PostgreSQL for durable games and action logs
- Caddy in front of rootless Podman Quadlet services on the VPS
The Phase 2 implementation provides the 82 available source-card counter fronts, 24 turns, reinforcement entry, automatic two-die combat results using verified unit factors, objective and casualty scoring, PostgreSQL state/actions/snapshots, restart-safe browser sessions, and a non-root production-shaped local container path. Reduced combat factors are owner-approved derived values: halve the full factor and round up, while a combat-one counter has one step and is eliminated by its first loss. All 253 owner-approved terrain records now feed rules and previews. Inferred movement/forest/hill connections and scenario adaptations are covered by the 2026-10-06 board/rules acceptance.
Use Node.js 24 and pnpm 11.21.0, with Bash, coreutils/util-linux, and age
(age plus age-keygen) available for local operations tests. These tests use
temporary keys and controlled container/SSH boundaries, not the VPS. Install
dependencies and run the terminating local gate with:
pnpm install --frozen-lockfile
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm smoke
pnpm exec playwright install --with-deps chromium
pnpm browser:acceptance
pnpm browser:movement
pnpm container:smokeFor interactive development, run pnpm dev. The web client listens on
http://127.0.0.1:5173 and proxies health, readiness, and WebSocket traffic to
the server on http://127.0.0.1:2567.
The browser acceptance command creates two isolated sessions at desktop and
tablet widths and writes rights-safe board captures to
test-results/browser-acceptance. pnpm browser:movement separately exercises
mandatory-rule edge cases in clearly labelled development-only browser fixtures.
Run GETTYSBURG_FULL_GAME=true pnpm browser:acceptance for the extended gate
used in CI: two independent live combat games through turn 24, one using desktop
keyboard controls and one using tablet touch. Both reload with server dice
awaiting confirmation, resolve available combat choices, verify final and
historical replay, and await durable cleanup. The games have an eight-minute
per-game bound; commands are paced below the room's abuse limit even on fast
runners. CI allows 25 minutes for these games plus the other required gates.
These automated checks do not replace owner tabletop adjudication.
Browser acceptance also records 20 click-to-confirmed-zoom/paint-opportunity
samples at each layout in desktop-performance.json and
tablet-performance.json. Strict mode (the default) fails the specified 100 ms
feedback budget when exceeded. Shared CI explicitly sets
GETTYSBURG_PERFORMANCE_MODE=report: misses retain their numeric evidence and
warnings without failing otherwise complete interactions. Missing confirmation,
input failure, and invalid configuration still fail in either mode.
These are unthrottled test-machine measurements, not broadband-load,
physical-device, cross-player-latency, or VPS-capacity claims.
Release acceptance still requires GETTYSBURG_PERFORMANCE_MODE=strict on
calibrated desktop hardware, recording hardware, OS/browser, workload, and
evidence. A green report-mode CI run does not close that gate.
Companion desktop-session-performance.json and tablet-session-performance.json
record one initial-lobby, reload/reconnect, and input-to-both-player observation
against the 3,000/5,000/500 ms targets. They include automation/render/network
overhead and do not establish percentiles or isolate Internet latency.
GETTYSBURG_BROWSER=firefox GETTYSBURG_FULL_GAME=true pnpm browser:acceptance
also passes locally at both widths with keyboard/mouse input. Chromium remains
the default and supplies continuous tablet-touch and native notification-worker
fixtures. The alternate-engine CI workflow additionally targets WebKit on Ubuntu;
actual Safari/iPad and real push-provider behavior remain manual release gates.
Locally managed WebKit runs use a task-owned HTTPS proxy (requires OpenSSL) so
production Secure session cookies survive reload. Other engines can exercise
that fixture with GETTYSBURG_BROWSER_HTTPS=true. Its self-signed certificate
exception applies only to fixture browser contexts; explicitly configured remote
origins retain certificate verification. No system trust store is changed.
The private source scans and Battle Manual remain local and ignored.
On the board, drag any friendly counter to move its entire stack. Hold Ctrl before dragging to move only the grabbed counter, or Ctrl-click once to keep it in single-counter mode for a later drag. Use the group selector for other legal subsets. Repeated drags must retain the exact group until a different group moves; previous movers then cannot resume that phase. Roads/rails cost half a point outside enemy ZOC; other costs use the pinned terrain and connections.
Reinforcement controls select a single counter or common-entry group and show the actual entry cost and blocked-entry alternatives. Retreat controls offer legal next steps, undo, and confirmation, including trapped losses and permitted edge exits. Advance controls offer legal victorious groups and destinations or decline; normal movement spending does not restrict a free advance. Board shortcuts remain available. Night guidance names counters that must withdraw. All movement and combat choices are coordinate-entry-free.
Choose View replay to inspect verified mandatory-game history. Opening, previous/next, event-number, and latest controls reconstruct a historical board; you can inspect either side and pan/zoom, but cannot move counters or resolve combat there. Recorded dice are shown with each combat snapshot. Return to live game restores the current authoritative view. Closing replay cancels its pending request; a failed authorized read clears the displayed snapshot. Older tabletop rulesets currently fail closed for replay rather than use newer rules, and deleted games remain inaccessible.
pnpm container:smoke builds the production-shaped image with Podman (or Docker
when Podman is unavailable), proves the application runs with a nonzero UID/GID,
checks PostgreSQL readiness and fail-closed mode, restarts the application, and
proves the saved browser session resumes before clean shutdown.
For an interactive same-origin proxy path, run:
install -d -m 0700 .secrets
if [[ ! -s .secrets/postgres-password ]]; then
umask 077
openssl rand -hex 32 >.secrets/postgres-password
fi
chmod 0444 .secrets/postgres-password
podman compose up --buildThe local Caddy endpoint is http://127.0.0.1:8080. The application container
uses DATABASE_URL, a persistent GETTYSBURG_CREDENTIAL_PEPPER_FILE,
GETTYSBURG_SERVER_HOST, GETTYSBURG_SERVER_PORT, the exact public origin in
GETTYSBURG_TRUSTED_ORIGIN, and the readiness-test switch
GETTYSBURG_REQUIRED_DEPENDENCY=unavailable. PostgreSQL and the credential
pepper use private persistent volumes; no runtime secret is committed.