added documentation

This commit is contained in:
2026-10-23 20:11:23 +02:00
parent 0913adaca5
commit 498df86962
10 changed files with 1285 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
# Architecture
## Overview
```mermaid
flowchart LR
subgraph Browser
UI[React app<br/>pages + canvas rendering]
KB[Keyboard input<br/>DAS / ARR]
HC[HubClient<br/>@microsoft/signalr]
KB --> HC
HC --> UI
end
subgraph Proxy[Reverse proxy - existing]
end
subgraph WebsiteContainer[actionblocks-website - nginx]
Static[index.html, JS, CSS,<br/>assets, config.js]
end
subgraph ServerContainer[actionblocks-server - ASP.NET Core]
Hub[GameHub]
LM[LobbyManager]
MR[MatchRunner<br/>60 Hz loop]
GL[ActionBlocks.Game<br/>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 |