Created
September 7, 2017 05:29
-
-
Save tylorr/639d904cd1ba715672406c1f2e378bcf to your computer and use it in GitHub Desktop.
Rust implementation for https://github.com/LearnProgramming/learntris
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::io; | |
use std::fmt; | |
extern crate ndarray; | |
use ndarray::{ArrayView2, aview2}; | |
use Block::*; | |
static I_BLOCKS: [[[Block; 4]; 4]; 4] = [ | |
[[E, E, E, E], | |
[C, C, C, C], | |
[E, E, E, E], | |
[E, E, E, E]], | |
[[E, E, C, E], | |
[E, E, C, E], | |
[E, E, C, E], | |
[E, E, C, E]], | |
[[E, E, E, E], | |
[E, E, E, E], | |
[C, C, C, C], | |
[E, E, E, E]], | |
[[E, C, E, E], | |
[E, C, E, E], | |
[E, C, E, E], | |
[E, C, E, E]], | |
]; | |
static O_BLOCK: [[Block; 2]; 2] = [[Y, Y], [Y, Y]]; | |
static Z_BLOCKS: [[[Block; 3]; 3]; 4] = [ | |
[[R, R, E], | |
[E, R, R], | |
[E, E, E]], | |
[[E, E, R], | |
[E, R, R], | |
[E, R, E]], | |
[[E, E, E], | |
[R, R, E], | |
[E, R, R]], | |
[[E, R, E], | |
[R, R, E], | |
[R, E, E]] | |
]; | |
static S_BLOCKS: [[[Block; 3]; 3]; 4] = [ | |
[[E, G, G], | |
[G, G, E], | |
[E, E, E]], | |
[[E, G, E], | |
[E, G, G], | |
[E, E, G]], | |
[[E, E, E], | |
[E, G, G], | |
[G, G, E]], | |
[[G, E, E], | |
[G, G, E], | |
[E, G, E]] | |
]; | |
static J_BLOCKS: [[[Block; 3]; 3]; 4] = [ | |
[[B, E, E], | |
[B, B, B], | |
[E, E, E]], | |
[[E, B, B], | |
[E, B, E], | |
[E, B, E]], | |
[[E, E, E], | |
[B, B, B], | |
[E, E, B]], | |
[[E, B, E], | |
[E, B, E], | |
[B, B, E]] | |
]; | |
static L_BLOCKS: [[[Block; 3]; 3]; 4] = [ | |
[[E, E, O], | |
[O, O, O], | |
[E, E, E]], | |
[[E, O, E], | |
[E, O, E], | |
[E, O, O]], | |
[[E, E, E], | |
[O, O, O], | |
[O, E, E]], | |
[[O, O, E], | |
[E, O, E], | |
[E, O, E]] | |
]; | |
static T_BLOCKS: [[[Block; 3]; 3]; 4] = [ | |
[[E, M, E], | |
[M, M, M], | |
[E, E, E]], | |
[[E, M, E], | |
[E, M, M], | |
[E, M, E]], | |
[[E, E, E], | |
[M, M, M], | |
[E, M, E]], | |
[[E, M, E], | |
[M, M, E], | |
[E, M, E]] | |
]; | |
#[derive(Copy,Clone,PartialEq)] | |
enum Block { | |
E, | |
M, | |
C, | |
B, | |
R, | |
G, | |
Y, | |
O, | |
} | |
#[derive(Copy,Clone,PartialEq)] | |
enum Tetramino { | |
T, | |
I, | |
J, | |
Z, | |
S, | |
O, | |
L, | |
} | |
enum Screen { | |
Title, | |
Pause, | |
Game, | |
} | |
struct State { | |
screen: Screen, | |
board: [[Block; 10]; 22], | |
score: i32, | |
cleared_lines: i32, | |
active_tetramino: Option<Tetramino>, | |
rotation: usize, | |
position: (i32, i32), | |
game_over: bool, | |
} | |
impl fmt::Display for Block { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
let c = match *self { | |
E => '.', | |
M => 'm', | |
C => 'c', | |
B => 'b', | |
R => 'r', | |
G => 'g', | |
Y => 'y', | |
O => 'o', | |
}; | |
write!(f, "{}", c) | |
} | |
} | |
fn main() { | |
let mut state = State { | |
screen: Screen::Game, | |
board: [[E; 10]; 22], | |
score: 0, | |
cleared_lines: 0, | |
active_tetramino: None, | |
rotation: 0, | |
position: (0, 0), | |
game_over: false, | |
}; | |
'game: loop { | |
let mut command = String::new(); | |
io::stdin().read_line(&mut command) | |
.expect("Failed to read line"); | |
let mut chars = command.chars(); | |
while let Some(c) = chars.next() { | |
match c { | |
'q' => break 'game, | |
' ' | '\n' | '\t' => continue, | |
_ => {} | |
} | |
match state.screen { | |
Screen::Game => handle_game_input(c, &mut chars, &mut state), | |
Screen::Title => handle_title_input(c, &mut state), | |
Screen::Pause => handle_pause_input(c, &mut state), | |
} | |
} | |
} | |
} | |
fn handle_game_input(c: char, chars: &mut std::str::Chars, state: &mut State) { | |
match c { | |
'@' => state.screen = Screen::Title, | |
'!' => state.screen = Screen::Pause, | |
'c' => clear_board(state), | |
'p' => print_board(state), | |
'P' => print_active_board(state), | |
'g' => state.board = read_board(), | |
's' => step(state), | |
't' => display_active_tetramino(state), | |
')' => rotate_right(state), | |
'(' => rotate_left(state), | |
';' => println!(), | |
'<' => shift_left(state), | |
'>' => shift_right(state), | |
'v' => shif_down(state), | |
'V' => drop(state), | |
'?' => | |
match chars.next() { | |
Some('s') => println!("{}", state.score), | |
Some('n') => println!("{}", state.cleared_lines), | |
_ => println!("Invalid query") | |
}, | |
'T' => set_active_tetramino(state, Tetramino::T), | |
'I' => set_active_tetramino(state, Tetramino::I), | |
'J' => set_active_tetramino(state, Tetramino::J), | |
'Z' => set_active_tetramino(state, Tetramino::Z), | |
'S' => set_active_tetramino(state, Tetramino::S), | |
'O' => set_active_tetramino(state, Tetramino::O), | |
'L' => set_active_tetramino(state, Tetramino::L), | |
_ => println!("Invalid game command") | |
} | |
} | |
fn handle_title_input(c: char, state: &mut State) { | |
match c { | |
'!' => state.screen = Screen::Game, | |
'p' => println!("Learntris (c) 1992 Tetraminex, Inc.\nPress start button to begin."), | |
_ => println!("Invalid title command") | |
} | |
} | |
fn handle_pause_input(c: char, state: &mut State) { | |
match c { | |
'!' => state.screen = Screen::Game, | |
'p' => println!("Paused\nPress start button to continue."), | |
_ => println!("Invalid pause command") | |
} | |
} | |
fn print_active_board(state: &State) { | |
let tview = get_tetramino_view(state.active_tetramino, state.rotation); | |
if let Some(v) = tview { | |
for (r, row) in state.board.iter().enumerate() { | |
for (c, board_block) in row.iter().enumerate() { | |
let local_x = (c as i32) - state.position.0; | |
let local_y = (r as i32) - state.position.1; | |
if local_x >= 0 && local_x < v.cols() as i32 && local_y >= 0 && local_y < v.rows() as i32 { | |
let tetramino_block = v[[local_y as usize, local_x as usize]]; | |
if tetramino_block != E { | |
print!("{} ", tetramino_block.to_string().to_uppercase()); | |
} else { | |
print!("{} ", board_block); | |
} | |
} else { | |
print!("{} ", board_block); | |
} | |
} | |
println!(); | |
} | |
if state.game_over { | |
println!("Game Over"); | |
} | |
} else { | |
print_board(state); | |
} | |
} | |
fn print_board(state: &State) { | |
for row in &state.board { | |
for block in row { | |
print!("{} ", block); | |
} | |
println!(); | |
} | |
if state.game_over { | |
println!("Game Over"); | |
} | |
} | |
fn read_board() -> [[Block; 10]; 22] { | |
let mut board = [[E; 10]; 22]; | |
for row in 0..22 { | |
let mut row_string = String::new(); | |
io::stdin().read_line(&mut row_string) | |
.expect("Failed to read row"); | |
let mut col = 0; | |
for c in row_string.chars() { | |
let block = match c { | |
'.' => E, | |
'm' => M, | |
'c' => C, | |
'b' => B, | |
'r' => R, | |
'g' => G, | |
'y' => Y, | |
'o' => O, | |
_ => continue, | |
}; | |
board[row][col] = block; | |
col += 1; | |
} | |
} | |
board | |
} | |
fn clear_board(state: &mut State) { | |
state.board = [[E; 10]; 22]; | |
state.game_over = false; | |
} | |
fn step(state: &mut State) { | |
for row in &mut state.board { | |
if !row.iter().any(|&x| x == E) { | |
state.score += 100; | |
state.cleared_lines += 1; | |
for block in row.iter_mut() { | |
*block = E | |
} | |
} | |
} | |
} | |
fn display_active_tetramino(state: &State) { | |
let view = get_tetramino_view(state.active_tetramino, state.rotation); | |
if let Some(v) = view { | |
for row in v.genrows() { | |
for block in row { | |
print!("{} ", block); | |
} | |
println!(); | |
} | |
} | |
} | |
fn set_active_tetramino(state: &mut State, t: Tetramino) { | |
state.active_tetramino = Some(t); | |
state.rotation = 0; | |
state.position = match t { | |
Tetramino::O => (4, 0), | |
_ => (3, 0) | |
}; | |
} | |
fn rotate_right(state: &mut State) { | |
attempt_rotation((state.rotation + 1) % 4, state); | |
} | |
fn rotate_left(state: &mut State) { | |
attempt_rotation((state.rotation + 4 - 1) % 4, state); | |
} | |
fn attempt_rotation(rotation: usize, state: &mut State) { | |
if let Some(tetramino) = state.active_tetramino { | |
let kick_offsets = if tetramino == Tetramino::I { | |
match (state.rotation, rotation) { | |
(0, 1) | (3, 2) => [( 0, 0), (-2, 0), ( 1, 0), (-2, 1), ( 1,-2)], | |
(1, 0) | (2, 3) => [( 0, 0), ( 2, 0), (-1, 0), ( 2,-1), (-1, 2)], | |
(1, 2) | (0, 3) => [( 0, 0), (-1, 0), ( 2, 0), (-1,-2), ( 2, 1)], | |
(2, 1) | (3, 0) => [( 0, 0), ( 1, 0), (-2, 0), ( 1, 2), (-2,-1)], | |
_ => panic!("Impossible rotation") | |
} | |
} else { | |
match (state.rotation, rotation) { | |
(0, 1) | (2, 1) => [( 0, 0), (-1, 0), (-1,-1), ( 0, 2), (-1, 2)], | |
(1, 0) | (1, 2) => [( 0, 0), ( 1, 0), ( 1, 1), ( 0,-2), ( 1,-2)], | |
(0, 3) | (2, 3) => [( 0, 0), ( 1, 0), ( 1,-1), ( 0, 2), ( 1, 2)], | |
(3, 0) | (3, 2) => [( 0, 0), (-1, 0), (-1, 1), ( 0,-2), (-1,-2)], | |
_ => panic!("Impossible rotation") | |
} | |
}; | |
// check for collision after rotation and all possible wallkick locations | |
for offset in kick_offsets.iter() { | |
let position = (state.position.0 + offset.0, state.position.1 + offset.1); | |
if check_collision(position, rotation, state) { | |
state.rotation = rotation; | |
state.position = position; | |
break; | |
} | |
} | |
} | |
} | |
fn shift_right(state: &mut State) { | |
let mut position = state.position.clone(); | |
position.0 = position.0 + 1; | |
if check_collision(position, state.rotation, state) { | |
state.position = position; | |
} | |
} | |
fn shift_left(state: &mut State) { | |
let mut position = state.position.clone(); | |
position.0 = position.0 - 1; | |
if check_collision(position, state.rotation, state) { | |
state.position = position; | |
} | |
} | |
fn shif_down(state: &mut State) { | |
let mut position = state.position.clone(); | |
position.1 = position.1 + 1; | |
if check_collision(position, state.rotation, state) { | |
state.position = position; | |
} else if state.position.1 == 0 { | |
state.game_over = true; | |
} | |
} | |
fn drop(state: &mut State) { | |
let mut position = state.position.clone(); | |
position.1 = position.1 + 1; | |
while check_collision(position, state.rotation, state) { | |
state.position = position; | |
position.1 = position.1 + 1; | |
} | |
if state.position.1 == 0 { | |
state.game_over = true; | |
} | |
commit_active_tetramino(state); | |
} | |
fn commit_active_tetramino(state: &mut State) { | |
let tview = get_tetramino_view(state.active_tetramino, state.rotation); | |
if let Some(v) = tview { | |
for ((r, c), elt) in v.indexed_iter() { | |
if *elt != E { | |
let abs_x = state.position.0 as usize + c; | |
let abs_y = state.position.1 as usize + r; | |
state.board[abs_y][abs_x] = *elt; | |
} | |
} | |
state.active_tetramino = None; | |
} | |
} | |
fn check_collision(position: (i32, i32), rotation: usize, state: &State) -> bool { | |
let tview = get_tetramino_view(state.active_tetramino, rotation); | |
if let Some(v) = tview { | |
for ((r, c), elt) in v.indexed_iter() { | |
if *elt != E { | |
let abs_x = position.0 + c as i32; | |
let abs_y = position.1 + r as i32; | |
if abs_x < 0 || abs_x >= 10 || abs_y < 0 || abs_y >= 22 { | |
return false; | |
} | |
if state.board[abs_y as usize][abs_x as usize] != E { | |
return false; | |
} | |
} | |
} | |
return true; | |
} | |
return false; | |
} | |
fn get_tetramino_view(tetramino: Option<Tetramino>, rotation: usize) -> Option<ArrayView2<'static, Block>> { | |
tetramino.map(|t| match t { | |
Tetramino::I => aview2(&I_BLOCKS[rotation]), | |
Tetramino::O => aview2(&O_BLOCK), | |
Tetramino::Z => aview2(&Z_BLOCKS[rotation]), | |
Tetramino::S => aview2(&S_BLOCKS[rotation]), | |
Tetramino::J => aview2(&J_BLOCKS[rotation]), | |
Tetramino::L => aview2(&L_BLOCKS[rotation]), | |
Tetramino::T => aview2(&T_BLOCKS[rotation]), | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment