-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame_state.rs
More file actions
392 lines (342 loc) · 11.6 KB
/
game_state.rs
File metadata and controls
392 lines (342 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#![allow(unknown_lints)]
#![allow(clippy::manual_is_multiple_of)]
use game_core::*;
use hecs::World;
use js_sys::Date;
use proto::*;
use std::collections::HashMap;
use worker::*;
/// Server-side match lifecycle state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchState {
/// Waiting for players to join
Waiting,
/// Both players connected, counting down
Countdown,
/// Game in progress
Playing,
/// Game ended
GameOver,
}
// Abstract connection for testing
pub trait GameClient {
fn send_bytes(&self, bytes: &[u8]) -> Result<()>;
}
impl GameClient for WebSocket {
fn send_bytes(&self, bytes: &[u8]) -> Result<()> {
self.send_with_bytes(bytes)
}
}
// Abstract environment (Time, Logging)
pub trait Environment {
fn now(&self) -> u64; // ms
fn log(&self, msg: String);
}
pub struct WasmEnv;
impl Environment for WasmEnv {
fn now(&self) -> u64 {
Date::now() as u64
}
fn log(&self, msg: String) {
// console_log! macro comes from worker crate and takes literal fmt string usually,
// but we can pass formatted string if we use "%s".
// Or actually console_log! invokes web_sys::console::log_1.
console_log!("{}", msg);
}
}
// Track client activity
pub struct ClientInfo {
pub client: Box<dyn GameClient>,
pub last_activity: u64, // Unix timestamp in seconds
}
// Game state wrapper for interior mutability
pub struct GameState {
pub env: Box<dyn Environment>,
pub world: World,
pub time: Time,
pub map: GameMap,
pub config: Config,
pub score: Score,
pub events: Events,
pub net_queue: NetQueue,
pub rng: GameRng,
pub respawn_state: RespawnState,
pub clients: HashMap<u8, ClientInfo>, // player_id (0=left, 1=right) -> ClientInfo
pub next_player_id: u8,
pub match_state: MatchState,
pub countdown_remaining: u8, // Countdown seconds remaining (3, 2, 1, 0)
pub tick: u32,
pub last_input: HashMap<u8, i8>, // Track last input per player to reduce logging
pub last_tick_time: u64, // Unix timestamp in ms
pub accumulator: f32, // For alarm loop catch-up timing
}
impl GameState {
pub fn new(env: Box<dyn Environment>) -> Self {
let mut world = World::new();
let map = GameMap::new();
let config = Config::new();
let time = Time::default();
let score = Score::new();
let events = Events::new();
let net_queue = NetQueue::new();
let rng = GameRng::default();
// Create ball at center
let ball_pos = map.ball_spawn();
let ball_vel = glam::Vec2::new(config.ball_speed_initial, 0.0);
create_ball(&mut world, ball_pos, ball_vel);
let now = env.now();
Self {
env,
world,
time,
map,
config,
score,
events,
net_queue,
rng,
respawn_state: RespawnState::new(),
clients: HashMap::new(),
next_player_id: 0,
match_state: MatchState::Waiting,
countdown_remaining: 3,
tick: 0,
last_input: HashMap::new(),
last_tick_time: now,
accumulator: 0.0,
}
}
/// Try to add a player. Returns (player_id, was_empty) if successful.
pub fn add_player(&mut self, client: Box<dyn GameClient>) -> Option<(u8, bool)> {
if self.clients.len() >= 2 {
return None;
}
let player_id = self.next_player_id;
self.next_player_id = (self.next_player_id + 1) % 2;
let was_empty = self.clients.is_empty();
let now = self.env.now() / 1000;
self.clients.insert(
player_id,
ClientInfo {
client,
last_activity: now,
},
);
// Spawn paddle
let paddle_y = self.map.paddle_spawn(player_id).y;
create_paddle(&mut self.world, player_id, paddle_y);
// Check if match can start
if self.clients.len() == 2 && self.match_state == MatchState::Waiting {
self.env
.log("DO: Both players connected, starting countdown".to_string());
self.match_state = MatchState::Countdown;
self.countdown_remaining = 3;
self.broadcast_to_all(&S2C::MatchFound);
}
Some((player_id, was_empty))
}
/// Broadcast a message to all connected clients
pub fn broadcast_to_all(&self, msg: &S2C) {
if let Ok(bytes) = msg.to_bytes() {
for client_info in self.clients.values() {
let _ = client_info.client.send_bytes(&bytes);
}
}
}
pub fn remove_player(&mut self, player_id: u8) {
self.clients.remove(&player_id);
// Despawn paddle
let entity_to_despawn =
self.world
.query::<(&Paddle,)>()
.iter()
.find_map(|(entity, (paddle,))| {
if paddle.player_id == player_id {
Some(entity)
} else {
None
}
});
if let Some(entity) = entity_to_despawn {
let _ = self.world.despawn(entity);
}
// Handle disconnection based on match state
match self.match_state {
MatchState::Playing => {
// Forfeit: remaining player wins
if let Some(&remaining_player) = self.clients.keys().next() {
self.broadcast_game_over(remaining_player);
}
self.match_state = MatchState::GameOver;
}
MatchState::Countdown => {
// Cancel countdown, notify remaining player
self.broadcast_to_all(&S2C::OpponentDisconnected);
self.match_state = MatchState::Waiting;
self.countdown_remaining = 3;
}
MatchState::GameOver => {
// Notify remaining player that opponent left (won't rematch)
self.broadcast_to_all(&S2C::OpponentDisconnected);
// Reset to waiting state
self.match_state = MatchState::Waiting;
}
MatchState::Waiting => {
// Just update state
if self.clients.is_empty() {
self.match_state = MatchState::Waiting;
}
}
}
}
pub fn handle_input(&mut self, player_id: u8, y: f32) {
if let Some(client_info) = self.clients.get_mut(&player_id) {
let now = self.env.now() / 1000;
client_info.last_activity = now;
self.net_queue.push_input(player_id, y);
}
}
/// Reset game state for a rematch
pub fn restart_match(&mut self) {
if self.match_state != MatchState::GameOver {
return;
}
self.env.log("DO: Restarting match".to_string());
// Reset game data
self.score = Score::new();
self.events = Events::new();
self.tick = 0;
self.last_input.clear();
self.net_queue = NetQueue::new();
self.accumulator = 0.0;
self.last_tick_time = self.env.now();
self.time = Time::default();
// Reset world entities (keep clients)
self.world.clear();
// Respawn ball
let ball_pos = self.map.ball_spawn();
let ball_vel = glam::Vec2::new(self.config.ball_speed_initial, 0.0);
create_ball(&mut self.world, ball_pos, ball_vel);
// Respawn paddles
for &player_id in self.clients.keys() {
let paddle_y = self.map.paddle_spawn(player_id).y;
create_paddle(&mut self.world, player_id, paddle_y);
}
// Set state to countdown
self.match_state = MatchState::Countdown;
self.countdown_remaining = 3;
// Notify clients
self.broadcast_to_all(&S2C::Countdown { seconds: 3 });
}
/// Process one countdown tick. Returns true if countdown finished.
pub fn tick_countdown(&mut self) -> bool {
if self.match_state != MatchState::Countdown {
return false;
}
if self.countdown_remaining > 0 {
self.broadcast_to_all(&S2C::Countdown {
seconds: self.countdown_remaining,
});
self.env
.log(format!("DO: Countdown: {}", self.countdown_remaining));
self.countdown_remaining -= 1;
false
} else {
// Countdown finished, start game
self.env
.log("DO: Countdown complete, starting game!".to_string());
self.match_state = MatchState::Playing;
self.broadcast_to_all(&S2C::GameStart);
true
}
}
pub fn step(&mut self) -> Option<u8> {
if self.match_state != MatchState::Playing {
return None;
}
self.time.dt = 0.016; // ~60 Hz
self.tick += 1;
if self.tick % 60 == 0 {
self.env.log(format!(
"DO: Game running, tick={}, clients={}",
self.tick,
self.clients.len()
));
}
game_core::step(
&mut self.world,
&mut self.time,
&self.map,
&self.config,
&mut self.score,
&mut self.events,
&mut self.net_queue,
&mut self.rng,
&mut self.respawn_state,
);
// Return winner if any
if let Some(winner) = self.score.has_winner(self.config.win_score) {
self.broadcast_game_over(winner);
self.match_state = MatchState::GameOver;
return Some(winner);
}
None
}
pub fn generate_state_message(&self) -> S2C {
// Get ball position and velocity
let (ball_x, ball_y, ball_vx, ball_vy) = self
.world
.query::<&Ball>()
.iter()
.next()
.map(|(_e, ball)| (ball.pos.x, ball.pos.y, ball.vel.x, ball.vel.y))
.unwrap_or((16.0, 12.0, 0.0, 0.0));
// Get paddle positions
let mut paddle_left_y = 12.0;
let mut paddle_right_y = 12.0;
let mut paddle_count = 0;
for (_e, paddle) in self.world.query::<&Paddle>().iter() {
paddle_count += 1;
if paddle.player_id == 0 {
paddle_left_y = paddle.y;
} else if paddle.player_id == 1 {
paddle_right_y = paddle.y;
}
}
if self.tick % 60 == 0 {
self.env.log(format!(
"DO: Paddle state - count={paddle_count}, left_y={paddle_left_y:.1}, right_y={paddle_right_y:.1}"
));
}
S2C::GameState(GameStateSnapshot {
tick: self.tick,
ball_x,
ball_y,
ball_vx,
ball_vy,
paddle_left_y,
paddle_right_y,
score_left: self.score.left,
score_right: self.score.right,
})
}
pub fn broadcast_state(&self) {
if self.clients.is_empty() {
return;
}
let state_msg = self.generate_state_message();
if let Ok(bytes) = state_msg.to_bytes() {
for client_info in self.clients.values() {
let _ = client_info.client.send_bytes(&bytes);
}
}
}
pub fn broadcast_game_over(&self, winner: u8) {
let msg = S2C::GameOver { winner };
if let Ok(bytes) = msg.to_bytes() {
for client_info in self.clients.values() {
let _ = client_info.client.send_bytes(&bytes);
}
}
}
}