# Architecture ## Overview ```mermaid flowchart LR subgraph Browser UI[React app
pages + canvas rendering] KB[Keyboard input
DAS / ARR] HC[HubClient
@microsoft/signalr] KB --> HC HC --> UI end subgraph Proxy[Reverse proxy - existing] end subgraph WebsiteContainer[actionblocks-website - nginx] Static[index.html, JS, CSS,
assets, config.js] end subgraph ServerContainer[actionblocks-server - ASP.NET Core] Hub[GameHub] LM[LobbyManager] MR[MatchRunner
60 Hz loop] GL[ActionBlocks.Game
Match / PlayerGame] Hub --> LM MR --> LM LM --> GL MR --> GL end Browser -- HTTPS --> Proxy Proxy -- static files --> WebsiteContainer Proxy -- WebSocket /hub --> ServerContainer ``` The **website container** only serves static files. All game communication goes from the browser straight to the **game server**, through the reverse proxy, over a SignalR WebSocket connection. The two containers never talk to each other. Each one sits in its own Docker network (`actionBlocks_website` and `actionBlocks_server`), and both networks are attached to the reverse proxy. ## Server-authoritative design The server owns the complete game state. The browser never simulates the game. It only: 1. sends **input commands** (`MoveLeft`, `HardDrop`, `UseItem`, …), and 2. **renders** the snapshots it receives. ```mermaid sequenceDiagram participant B as Browser participant H as GameHub participant M as Match (under lobby lock) participant R as MatchRunner (60 Hz) B->>H: Input("MoveLeft") H->>M: HandleInput(playerId, MoveLeft) Note over H: lobby.Dirty = true loop every ~16.7 ms R->>M: Update(delta) R->>M: DrainEvents() alt every 2nd tick, or Dirty, or events present R-->>B: MatchState(snapshot) end end ``` **Why this approach:** - **One source of truth.** Interactions between players (garbage, item effects, board swaps) are resolved in one place. Nothing ever needs to be reconciled. - **Cheating is mostly impossible.** Clients cannot fake line clears, items or piece positions. - **Game logic is easy to change.** All rules live in one C# library with unit tests. **Trade-off:** input feels as responsive as the network round trip allows. There is no client-side prediction. On a LAN or a good connection (< 50 ms) this is fine. For high-latency play, the natural next step would be local prediction of the player's own active piece. See [Known limitations](#known-limitations). To keep the feel responsive anyway: - **Auto-repeat on the client.** Delayed auto shift (DAS) and the auto-repeat rate (ARR) for held left/right keys run in the browser. The browser sends discrete `MoveLeft` / `MoveRight` commands at the repeat rate. - **Extra snapshot after input.** Any input marks the lobby *dirty*. The next tick (≤ 16.7 ms later) then always sends a snapshot, even when it isn't a regular broadcast tick. ## Server internals at a glance The C# solution separates **game rules** from **hosting**: | Project | Depends on | Contains | |---|---|---| | `ActionBlocks.Game` | nothing (plain .NET) | Board, pieces, rotation system, randomizer, `PlayerGame`, `Match`, garbage, items, targeting | | `ActionBlocks.Server` | `ActionBlocks.Game`, ASP.NET Core | SignalR hub, lobbies, tick loop, network DTOs | | `ActionBlocks.Game.Tests` | `ActionBlocks.Game`, xUnit | Unit tests | Because `ActionBlocks.Game` knows nothing about networking, it can be unit tested in isolation and reused, for example for bots or a different transport. Rules are modular through interfaces. Swap an implementation to change a rule without touching the rest: | Interface | Default implementation | Decides | |---|---|---| | `IRotationSystem` | `SuperRotationSystem` | How pieces rotate and kick off walls/floor | | `IRandomizer` | `SevenBagRandomizer` | The piece sequence | | `IGarbageCalculator` | `StandardGarbageCalculator` | How many lines an attack sends | | `IScoreCalculator` | `StandardScoreCalculator` | Points per clear | | `IGarbageTargeter` | `CyclingGarbageTargeter` | Who receives garbage | | `IItem` | 5 item classes | What an item does | Numeric rules (board size, lock delay, item chance, …) live in the `GameSettings` record. They can be overridden through configuration without recompiling. ## Concurrency model - `LobbyManager` stores lobbies in a `ConcurrentDictionary`. - Each `Lobby` has a `Sync` object. **Every access to a lobby and its `Match` happens inside `lock (lobby.Sync)`**: hub calls (input, join, leave, start) and the tick loop. - `Match` and `PlayerGame` are deliberately **not thread-safe**. They rely on the lobby lock. - A single background service, `MatchRunner`, ticks all lobbies sequentially at 60 Hz. Network sends happen **outside** the lock and are fire-and-forget, so a slow client never blocks the game loop. ## State lifetime All state is **in memory**: there is no database. If the server process restarts, every lobby and match is lost. If a player's connection drops, they are removed from their lobby and forfeit any running match. Reconnecting creates a new connection, so the player has to join again. ## Known limitations These are conscious simplifications. They are good candidates for future work: | Limitation | Impact | Possible improvement | |---|---|---| | No client-side prediction | Movement latency equals the network round trip | Predict the own active piece locally and reconcile with snapshots | | Full JSON snapshots of all players at ~30–60 Hz | About 1.5 KB per snapshot for 2 players. It grows linearly with players | Send deltas, or use the MessagePack protocol, or send opponents at a lower rate | | Blackout (invisibility) is hidden client-side | A modified client could still show the board | Strip the board from that player's personal snapshot on the server | | No reconnect into a running lobby | A network hiccup kicks the player | Issue a session token and re-bind to the player on reconnect | | In-memory lobbies, single server instance | No horizontal scaling | Use a SignalR backplane plus external state, or sticky sessions per lobby | | Max 8 players per lobby | Matches the number keys 1–9 (1 = self) | Raise `Lobby.MaxPlayers`; the UI would need paging for targets |