Skip to content

Commit b0c0789

Browse files
committed
Refactor lobby_worker and configure auto-formatting
- Separate inline CSS and JS from lobby_worker/index.html - Update build scripts to include separate asset files - Configure Prettier and lint-staged for automated formatting - Enable Rust auto-formatting via rustfmt - Apply formatting to entire project
1 parent be8f124 commit b0c0789

File tree

12 files changed

+2147
-2007
lines changed

12 files changed

+2147
-2007
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ jobs:
6969
- name: Setup Node.js
7070
uses: actions/setup-node@v4
7171
with:
72-
node-version: '20'
72+
node-version: "20"
7373

7474
# No npm ci needed as there are no dependencies in package.json
7575

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
npx lint-staged
12
cargo fmt --check
23
cargo check --workspace
34
cargo clippy --workspace -- -D warnings

.prettierignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
worker/pkg/
2+
target/
3+
dist/
4+
node_modules/
5+
.wrangler/

.prettierrc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"semi": true,
3+
"tabWidth": 2,
4+
"singleQuote": false,
5+
"printWidth": 100,
6+
"trailingComma": "es5"
7+
}

ARCHITECTURE.md

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@ Pongo is a real-time multiplayer game built on a shared-code architecture. The c
1111
```mermaid
1212
graph TD
1313
Client["Client (Browser)"] <-->|WebSocket Binary| Server["Cloudflare Worker"]
14-
14+
1515
subgraph "Server (Durable Object)"
1616
ServerLoop["Game Loop (60Hz)"]
1717
ServerState["Authoritative State"]
1818
end
19-
19+
2020
subgraph "Client (WASM)"
2121
Input["Input Handling"] --> Predict["Prediction System"]
2222
Predict --> Render["WebGPU Renderer"]
2323
ServerState -.->|Snapshot| Reconcile["Reconciliation"]
2424
end
25-
25+
2626
ServerLoop -- Broadcast 20Hz --> Client
2727
Client -- Input --> ServerLoop
2828
```
@@ -31,12 +31,12 @@ graph TD
3131

3232
The project is structured as a Cargo workspace with shared crates.
3333

34-
| Crate | Path | Description | Key Files |
35-
|-------|------|-------------|-----------|
36-
| **game_core** | [`game_core/`](game_core/) | **The Heart.** Shared ECS logic, physics, and config. | [`lib.rs`](game_core/src/lib.rs) (step function)<br>[`config.rs`](game_core/src/config.rs) (constants) |
37-
| **client_wasm** | [`client_wasm/`](client_wasm/) | **The Frontend.** Prediction, interpolation, and rendering. | [`lib.rs`](client_wasm/src/lib.rs) (entry)<br>[`renderer/`](client_wasm/src/renderer) (WebGPU) |
38-
| **server_do** | [`server_do/`](server_do/) | **The Backend.** Durable Object implementation. | [`game_state.rs`](server_do/src/game_state.rs) (server logic) |
39-
| **proto** | [`proto/`](proto/) | **The Glue.** Network messages and serialization. | [`lib.rs`](proto/src/lib.rs) (structs) |
34+
| Crate | Path | Description | Key Files |
35+
| --------------- | ------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
36+
| **game_core** | [`game_core/`](game_core/) | **The Heart.** Shared ECS logic, physics, and config. | [`lib.rs`](game_core/src/lib.rs) (step function)<br>[`config.rs`](game_core/src/config.rs) (constants) |
37+
| **client_wasm** | [`client_wasm/`](client_wasm/) | **The Frontend.** Prediction, interpolation, and rendering. | [`lib.rs`](client_wasm/src/lib.rs) (entry)<br>[`renderer/`](client_wasm/src/renderer) (WebGPU) |
38+
| **server_do** | [`server_do/`](server_do/) | **The Backend.** Durable Object implementation. | [`game_state.rs`](server_do/src/game_state.rs) (server logic) |
39+
| **proto** | [`proto/`](proto/) | **The Glue.** Network messages and serialization. | [`lib.rs`](proto/src/lib.rs) (structs) |
4040

4141
---
4242

@@ -122,13 +122,15 @@ graph TD
122122
## Key Data Flows
123123

124124
### Input Handling
125+
125126
1. Browser captures key press in [`on_key_down`](client_wasm/src/lib.rs).
126127
2. Client updates local paddle immediately.
127128
3. Client sends `C2S::Input` to server.
128129
4. Server validates input (enforcing speed limits) and updates entity intent.
129130
5. Server includes new paddle position in next broadcast.
130131

131132
### Rendering Frame
133+
132134
1. `requestAnimationFrame` calls [`render`](client_wasm/src/lib.rs).
133135
2. Prediction system updates local game state.
134136
3. [`Renderer::draw`](client_wasm/src/renderer/mod.rs) submits draw commands to GPU.
@@ -139,21 +141,22 @@ graph TD
139141

140142
### Game Constants
141143

142-
| Constant | Value | Unit |
143-
|----------|-------|------|
144-
| Arena | 32 × 24 | units |
145-
| Paddle | 0.8 × 4.0 | units |
146-
| Paddle speed | 18 | units/sec |
147-
| Ball radius | 0.5 | units |
148-
| Ball speed | 12 → 24 | units/sec |
149-
| Speed multiplier | 1.05× | per hit |
150-
| Win score | 5 | points |
144+
| Constant | Value | Unit |
145+
| ---------------- | --------- | --------- |
146+
| Arena | 32 × 24 | units |
147+
| Paddle | 0.8 × 4.0 | units |
148+
| Paddle speed | 18 | units/sec |
149+
| Ball radius | 0.5 | units |
150+
| Ball speed | 12 → 24 | units/sec |
151+
| Speed multiplier | 1.05× | per hit |
152+
| Win score | 5 | points |
151153

152-
*Constants defined in [`game_core/src/config.rs`](game_core/src/config.rs)*
154+
_Constants defined in [`game_core/src/config.rs`](game_core/src/config.rs)_
153155

154156
### Network Protocol
155157

156158
**Client → Server:**
159+
157160
```rust
158161
enum C2S {
159162
Join { code: [u8; 5] },
@@ -163,6 +166,7 @@ enum C2S {
163166
```
164167

165168
**Server → Client:**
169+
166170
```rust
167171
enum S2C {
168172
Welcome { player_id: u8 },

ARTICLE.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
# How I Built a Real-Time Multiplayer Game on the Edge with Rust and WebAssembly
22

3-
> *Real-time multiplayer games are mostly about trade-offs: latency vs authority, simplicity vs correctness, and cost vs control. This article walks through an approach that worked well for me — using Rust compiled to WebAssembly to share deterministic game logic between the browser and Cloudflare’s edge.*
3+
> _Real-time multiplayer games are mostly about trade-offs: latency vs authority, simplicity vs correctness, and cost vs control. This article walks through an approach that worked well for me — using Rust compiled to WebAssembly to share deterministic game logic between the browser and Cloudflare’s edge._
44
55
**[Play the live demo →](https://pongo.tre.systems/)** | **[View source on GitHub →](https://github.com/rgilks/pongo)**
66

77
---
88

99
## Why Pong?
1010

11-
In 1972, Atari released *Pong*, effectively kicking off the video game industry. More than fifty years later, it turns out to be a great test case for multiplayer networking.
11+
In 1972, Atari released _Pong_, effectively kicking off the video game industry. More than fifty years later, it turns out to be a great test case for multiplayer networking.
1212

1313
In many modern games, latency can be masked with animation, camera tricks, or generous hitboxes. Pong gives you none of that. The physics are simple, the ball is fast, and if your paddle isn’t exactly where you expect it to be, you feel it immediately.
1414

1515
It demands:
16+
1617
1. **Precise movement** — high-frequency input sampling.
1718
2. **Instant feedback** — minimal perceived latency.
1819
3. **State validation** — preventing the client and server from drifting apart.
@@ -29,7 +30,7 @@ When building **Pongo**, my goal wasn’t just to recreate a classic — it was
2930

3031
The solution I landed on was a kind of “universal app” architecture, built with **Rust**, **WebAssembly (WASM)**, and **Cloudflare Durable Objects**.
3132

32-
Most multiplayer games try to share logic between client and server, but language boundaries usually get in the way. By writing the core game logic in Rust, I can compile it to WebAssembly and run the *same code* in two very different environments:
33+
Most multiplayer games try to share logic between client and server, but language boundaries usually get in the way. By writing the core game logic in Rust, I can compile it to WebAssembly and run the _same code_ in two very different environments:
3334

3435
1. **The Browser** — rendering at 120Hz+ with WebGPU.
3536
2. **The Edge** — running inside a Cloudflare Durable Object at a fixed 60Hz.
@@ -51,7 +52,7 @@ graph TD
5152
Input["Player Input"]
5253
Predict["Client Predictor\n(Prediction)"]
5354
Render["WebGPU Renderer"]
54-
55+
5556
Input --> Predict
5657
Predict -->|"Step (Frame Rate)"| Logic
5758
Predict --> Render
@@ -108,7 +109,7 @@ Each match runs inside a **Cloudflare Durable Object**. A Durable Object is a si
108109
This is the key difference from typical stateless serverless functions: a Durable Object can host a proper **game loop**.
109110

110111
```mermaid
111-
flowchart
112+
flowchart
112113
Inputs["Client Inputs"] -->|"WebSocket"| DO["Durable Object"]
113114
DO -->|"60Hz"| Step["game_core::step()"]
114115
Step -->|"20Hz"| Broadcast["State Snapshots"]
@@ -142,14 +143,14 @@ sequenceDiagram
142143
Note over Client: Frame 100: User presses UP
143144
Client->>Client: Apply Input (Predict)
144145
Client->>Server: Send Input
145-
146+
146147
Note over Server: ...Network Delay...
147-
148+
148149
Server->>Server: Process Input
149150
Server->>Client: Send Snapshot (Tick 100)
150-
151+
151152
Note over Client: ...Network Delay...
152-
153+
153154
Note over Client: Client receives snapshot
154155
Client->>Client: Compare prediction vs server
155156
alt Prediction matches

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ npm run build && npm run dev # http://localhost:8787
2020

2121
## How to Play
2222

23-
| Mode | How |
24-
|------|-----|
23+
| Mode | How |
24+
| --------------- | --------------------------------------- |
2525
| **Multiplayer** | Click **CHALLENGE** → share link → JOIN |
26-
| **VS AI** | Click **PLAY** |
26+
| **VS AI** | Click **PLAY** |
2727

2828
**Controls:** Arrow keys or W/S · Touch on mobile
2929
**Rules:** First to 5. Hit position affects ball trajectory.
@@ -33,6 +33,7 @@ npm run build && npm run dev # http://localhost:8787
3333
See **[ARCHITECTURE.md](ARCHITECTURE.md)** for the system diagram and component deep dive.
3434

3535
**Key design decisions:**
36+
3637
- **Shared `game_core`** — Same ECS physics on client and server for prediction
3738
- **Binary protocol** — Minimal `postcard` serialization over WebSocket
3839
- **Durable Objects** — Each match is a stateful instance with 60Hz game loop
@@ -54,17 +55,17 @@ pongo/
5455
```bash
5556
npm run build # Build WASM
5657
npm run dev # Local server
57-
npm run test # Run tests
58+
npm run test # Run tests
5859
npm run deploy # Deploy to Cloudflare
5960
```
6061

6162
## Troubleshooting
6263

63-
| Issue | Fix |
64-
|-------|-----|
65-
| Build fails | `cargo install wasm-pack` |
64+
| Issue | Fix |
65+
| ----------- | ------------------------------------ |
66+
| Build fails | `cargo install wasm-pack` |
6667
| Port in use | Kill process or edit `wrangler.toml` |
67-
| Reset state | Delete `.wrangler/state/` |
68+
| Reset state | Delete `.wrangler/state/` |
6869

6970
See **[ARCHITECTURE.md](ARCHITECTURE.md)** for technical details and game constants.
7071

0 commit comments

Comments
 (0)