Most implementations of 2048 use a matrix to represent the locations and values of different cells. To implement board shifts they rotate the matrix and apply the same function, then rotate it back.
With the way we're using Bevy's ECS we are going to take a different approach and we'll be using simpler math.
The algorithm we'll be using will order all of the tiles from the bottom left to the top right. So if every grid location is filled with a tile, we'd get the tile at 0,0
, then 1,0
, then 2,0
, then 3,0
. After reaching the end we go up one level and the fifth tile is 1,0
, and so on.
With this arrangement of tiles, we can check the current tile and the next tile and see if they should be merged. If they're on different rows, or if their points values are different we skip forward to the next tile, otherwise we merge them.
We'll add a query for all the tile entities to the board_shift
system. We are going to potentially mutate the Position
and Points
components on the tile entity depending so they need to be mutable references.
fn board_shift(
input: Res<Input<KeyCode>>,
mut tiles: Query<(Entity, &mut Position, &mut Points)>,
)
The first direction we're going to implement is BoardShift::Left
. To start off, we'll make sure the tiles are sorted into the order we talked about earlier.
The tiles
query is mutable, so we need to use iter_mut
to get an iterator. After getting an iterator, we can use .sorted_by
to first sort by row and then column of the grid. .sorted_by
is from the Itertools crate.
.sorted_by
expects an Ordering
value as the return type. Ordering
is an enum that has Less
, Equal
, and Greater
variants.
Ord
is a trait that can be implemented for any type. It requires implementing the cmp
method, which compares two values of the given type. In our case, u8
already has an Ord
implementation, so we can use Ord::cmp
to determine if any two y
values in a tile's Position
component are equal or different.
If they're equal, that means the two tiles are in the same row and we need to do the same for the x
value. If they're not equal, we can pass the return value back to sorted_by
because it will be an Ordering
variant.
Some(BoardShift::Left) => {
dbg!("left");
let mut it =
tiles.iter_mut().sorted_by(|a, b| {
match Ord::cmp(&a.1.y, &b.1.y) {
Ordering::Equal => {
Ord::cmp(&a.1.x, &b.1.x)
}
ordering => ordering,
}
});
}
While the Ord
trait is part of Rust's prelude and is already in scope for use to use: We need to bring Ordering
ourselves, which we can add at the top of the file.
use std::cmp::Ordering;
Aside: Debugging
If this is confusing, you can do something to help. Add the following line after the sorting code. This will collect the iterator into a Vec
and debug it to the console for you to look at when you hit the left arrow key. The _
in the type signature tells Rust to "figure out what the type should be here", which it will be able to grab from our query.
dbg!(it.collect::<Vec<_>>());
If you have not done so already, you will also need to add a Debug
representation to the Points
and Position
structs to use the above dbg!
code.
#[derive(Debug, Component)]
struct Points {
value: u32,
}
#[derive(Debug, Component)]
struct Position {
x: u8,
y: u8,
}
This prints out a tuple for each tile that matches what we asked for in the query type signature: (Entity, &mut Position, &mut Points)
. If you run this a few times you will be able to see the tiles on the screen and compare them to the output we're getting from the sorting.
[src/main.rs:247] "left" = "left"
[src/main.rs:257] it.collect::<Vec<_>>() = [
(
21v0,
Mut(
Position {
x: 1,
y: 0,
},
),
Mut(
Points {
value: 2,
},
),
),
(
19v0,
Mut(
Position {
x: 0,
y: 2,
},
),
Mut(
Points {
value: 2,
},
),
),
]
After you're done debugging, remember to remove that dbg!
line of code or you'll start seeing some "borrow of moved value: it
" warnings as we continue. The derive lines can stay.