Skip to content

Commit

Permalink
Fix system ordering and add game over test
Browse files Browse the repository at this point in the history
System ordering turns out to be a critical part of 2048 logic. Specifically
the most relevant ordering is:

1. board shift
2. new tile addition

I've added chain to more than is necessary here, but since it is in no
way performance sensitive, it might be easier than running "as much as possible"
in parallel.

This commit also adds an example testing the game over system and adds
some cfg(test) flagged functionality to support that testing.
  • Loading branch information
ChristopherBiscardi committed Feb 23, 2024
1 parent 447b621 commit 1687150
Showing 1 changed file with 94 additions and 8 deletions.
102 changes: 94 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ struct Position {
#[derive(Component)]
pub struct TileText;

#[derive(Debug)]
enum BoardShift {
Left,
Right,
Expand Down Expand Up @@ -186,12 +187,13 @@ fn main() {
.add_systems(
Update,
(
render_tile_points,
board_shift,
render_tiles,
new_tile_handler,
render_tile_points,
render_tiles,
end_game,
)
.chain()
.run_if(in_state(RunState::Playing)),
)
.add_systems(
Expand Down Expand Up @@ -223,8 +225,8 @@ fn spawn_board(mut commands: Commands, board: Res<Board>) {
builder.spawn(SpriteBundle {
sprite: Sprite {
color: MATERIALS.tile_placeholder,
custom_size: Some(Vec2::new(
TILE_SIZE, TILE_SIZE,
custom_size: Some(Vec2::splat(
TILE_SIZE,
)),
..default()
},
Expand All @@ -249,7 +251,15 @@ fn spawn_tiles(mut commands: Commands, board: Res<Board>) {
board.tiles().choose_multiple(&mut rng, 2);
for (x, y) in starting_tiles.iter() {
let pos = Position { x: *x, y: *y };
spawn_tile(&mut commands, &board, pos);
spawn_tile(
&mut commands,
&board,
pos,
#[cfg(test)]
{
Points { value: 2 }
},
);
}
}

Expand Down Expand Up @@ -402,7 +412,15 @@ fn new_tile_handler(
.choose(&mut rng);

if let Some(pos) = possible_position {
spawn_tile(&mut commands, &board, pos);
spawn_tile(
&mut commands,
&board,
pos,
#[cfg(test)]
{
Points { value: 2 }
},
);
}
}
}
Expand All @@ -411,6 +429,7 @@ fn spawn_tile(
commands: &mut Commands,
board: &Board,
pos: Position,
#[cfg(test)] points: Points,
) {
commands
.spawn((
Expand All @@ -429,7 +448,14 @@ fn spawn_tile(
),
..default()
},
Points { value: 2 },
#[cfg(test)]
{
points
},
#[cfg(not(test))]
{
Points { value: 2 }
},
pos,
))
.with_children(|child_builder| {
Expand Down Expand Up @@ -490,7 +516,6 @@ fn end_game(
);

if !has_move {
dbg!("game over!");
next_state.set(RunState::GameOver);
}
};
Expand All @@ -506,3 +531,64 @@ fn game_reset(
}
game.score = 0;
}

#[cfg(test)]
mod tests {
use bevy::ecs::system::CommandQueue;

use super::*;

#[test]
fn gameover_triggers_when_16_tiles_exist() {
let mut app = App::new();
let board = Board::new(4);
app.insert_resource(Board::new(4))
.init_state::<RunState>()
.add_systems(Startup, (spawn_board).chain())
.add_systems(
Update,
(
end_game,
// apply_state_transition here is
// optional, but would require an
// additional
// app.update() cycle to run if we
// didn't include it here.
apply_state_transition::<RunState>,
)
.chain(),
);

// insert tiles to set up a game
let mut command_queue = CommandQueue::default();

let mut commands =
Commands::new(&mut command_queue, &app.world);
for (i, (x, y)) in board.tiles().enumerate() {
spawn_tile(
&mut commands,
&board,
Position { x, y },
Points {
value: 2_u32.pow(i as u32),
},
);
}

command_queue.apply(&mut app.world);

// Run systems to insert tiles.
// Game over is also detected immediately, but the
// NextState resource must be used after
// it is inserted by `apply_state_transition`
// system
app.update();

let state = app
.world
.get_resource::<State<RunState>>()
.expect("state to be inserted");

assert_eq!(&RunState::GameOver, state.get());
}
}

0 comments on commit 1687150

Please sign in to comment.