Thanks to how we created the Tetrimino type, it's quite easy to do:
impl Tetrimino { fn rotate(&mut self) { self.current_state += 1; if self.current_state as usize >= self.states.len() { self.current_state = 0; } } }
And we're done. However, we don't check anything: what happens if there is a block already used by another tetrimino? We'll just overwrite it. Such a thing cannot be accepted!
In order to perform this check, we'll need the game map as well. It's simply a vector line and a line is a vector of u8. Or, more simply:
Vec<Vec<u8>>
Considering that it isn't too hard to read, we'll just keep it this way. Now let's write the method:
fn test_position(&self, game_map: &[Vec<u8>], tmp_state: usize, x: isize, y: usize) -> bool { for decal_y in 0..4 { for decal_x in 0..4 { let x = x + decal_x; if self.states[tmp_state][decal_y][decal_x as usize] != 0
&& (y + decal_y >= game_map.len() || x < 0 || x as usize >= game_map[y + decal_y].len() || game_map[y + decal_y][x as usize] != 0) { return false; } } } return true; }
Before explaining this function, it seems important to explain why the game map became a &[Vec<u8>]. When you send a non-mutable reference over a vector (Vec<T>), it is then dereferenced into a &[T] slice, which is a constant view over the vector's content.
And we're done (for this method)! Now time for explanations: we loop over every block of our tetrimino and check whether the block is free in the game map (by checking whether it is equal to 0) and if it isn't going out of the game map.
Now that we have our test_position method, we can update the rotate method:
fn rotate(&mut self, game_map: &[Vec<u8>]) { let mut tmp_state = self.current_state + 1; if tmp_state as usize >= self.states.len() { tmp_state = 0; } let x_pos = [0, -1, 1, -2, 2, -3]; for x in x_pos.iter() { if self.test_position(game_map, tmp_state as usize, self.x + x, self.y) == true { self.current_state = tmp_state; self.x += *x; break } } }
A bit longer, indeed. Since we can't be sure that the piece will be put where we want it to go, we need to make temporary variables and then check the possibilities. Let's go through the code:
let mut tmp_state = self.current_state + 1; if tmp_state as usize >= self.states.len() { tmp_state = 0; }
This is exactly what our rotate method did before, except that now, we use temporary variables before going further:
let x_pos = [0, -1, 1, -2, 2, -3];
This line on its own doesn't make much sense but it'll be very useful next: in case the piece cannot be placed where we want, we try to move it on the x axis to see if it'd work in some other place. It allows you to have a Tetris that is much more flexible and comfortable to play:
for x in x_pos.iter() { if self.test_position(game_map, tmp_state as usize, self.x + x, self.y) == true { self.current_state = tmp_state; self.x += *x; break } }
With the explanations given previously, this loop should be really easy to understand. For each x shift, we check whether the piece can be placed there. If it works, we change the values of our tetrimino, otherwise we just continue.
If no x shift worked, we just leave the function without doing anything.
Now that we can rotate and test the position of a tetrimino, it'd be nice to actually move it as well (when the timer goes to 0 and the tetrimino needs to go down, for example). The main difference with the rotate method will be that, if the tetrimino cannot move, we'll return a Boolean value to allow the caller to be aware of it.
So the method looks like this:
fn change_position(&mut self, game_map: &[Vec<u8>], new_x: isize, new_y: usize) -> bool { if self.test_position(game_map, self.current_state as usize,
new_x, new_y) == true { self.x = new_x as isize; self.y = new_y; true } else { false } }
Another difference that you have certainly already spotted is that we don't check multiple possible positions, just the one received. The reason is simple; contrary to a rotation, we can't move the tetrimino around when it receives a move instruction. Imagine asking the tetrimino to move to the right and it doesn't move, or worse, it moves to the left! We can't allow it and so we're not doing it.
Now about the method's code: it's very simple. If we can put the tetrimino in a place, we update the position of the tetrimino and return true, otherwise, we do nothing other than return false.
Most of the work is performed in the test_position method, allowing our method to be really small.
With these three methods, we have almost everything we need. But for even more simplicity in the future, let's add one more:
fn test_current_position(&self, game_map: &[Vec<u8>]) -> bool { self.test_position(game_map, self.current_state as usize,
self.x, self.y) }
We'll use it when we generate a new tetrimino: if it cannot be placed where it appeared because another tetrimino is already there, it means the game is over.
We can now say that our Tetrimino type is fully implemented. Congratulations! Time to start the game type!