Created
November 23, 2015 04:08
-
-
Save Veedrac/7e009819107633833bd8 to your computer and use it in GitHub Desktop.
Edits and commentary on https://www.reddit.com/r/rust/comments/3tqkxl/ported_my_score4_engine_to_functionalstyle_rust/
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
pub const WIDTH: usize = 7; | |
pub const HEIGHT: usize = 6; | |
pub const MAX_DEPTH: i32 = 7; | |
pub const ORANGE_WINS: i32 = 1000000; | |
pub const YELLOW_WINS: i32 = -ORANGE_WINS; | |
// Couldn't resist changing the type of this | |
pub type Board = [[i8; WIDTH]; HEIGHT]; | |
pub fn other_color(color:i8) -> i8 { | |
-color | |
} | |
// += makes this noticably nicer, | |
// as does using correct types for | |
// dimensions. I also inlined counts[...] += 1. | |
// That's about it for this one, though. | |
pub fn score_board(board:&Board) -> i32 { | |
let mut counts = [0; 9]; | |
// Horizontal spans | |
for y in 0..HEIGHT { | |
let mut score: i8 = board[y][0] + board[y][1] + board[y][2]; | |
for x in 3..WIDTH { | |
score += board[y][x]; | |
counts[(score+4) as usize] += 1; | |
score -= board[y][x - 3]; | |
} | |
} | |
// Vertical spans | |
for x in 0..WIDTH { | |
let mut score: i8 = board[0][x] + board[1][x] + board[2][x]; | |
for y in 3..HEIGHT { | |
score += board[y][x]; | |
counts[(score+4) as usize] += 1; | |
score -= board[y - 3][x]; | |
} | |
} | |
// Down-right (and up-left) diagonals | |
for y in 0..HEIGHT-3 { | |
for x in 0 .. WIDTH-3 { | |
let mut score: i8 = 0; | |
for idx in 0..4 { | |
score += board[y+idx][x+idx]; | |
} | |
counts[(score+4) as usize] += 1; | |
} | |
} | |
// up-right (and down-left) diagonals | |
for y in 3..HEIGHT { | |
for x in 0..WIDTH-3 { | |
let mut score: i8 = 0; | |
for idx in 0..4 { | |
score += board[y-idx][x+idx]; | |
} | |
counts[(score+4) as usize] += 1; | |
} | |
} | |
if counts[0] != 0 { | |
YELLOW_WINS | |
} else if counts[8] != 0 { | |
ORANGE_WINS | |
} else { | |
counts[5] + 2*counts[6] + 5*counts[7] - | |
counts[3] - 2*counts[2] - 5*counts[1] | |
} | |
} | |
/* vim: set expandtab ts=8 sts=4 shiftwidth=4 */ |
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::env; | |
use std::process; | |
mod common; | |
use common::Board; | |
use common::{score_board, other_color}; | |
use common::{WIDTH, HEIGHT, ORANGE_WINS, YELLOW_WINS, MAX_DEPTH}; | |
// Parse cmdline specs to create initial board | |
fn load_board(args: Vec<String>) -> Board { | |
let mut board = [[0; WIDTH]; HEIGHT]; | |
for y in 0..HEIGHT { | |
for x in 0..WIDTH { | |
let orange = format!("o{}{}", y, x); | |
let yellow = format!("y{}{}", y, x); | |
if args.iter().any(|x| *x == orange) { | |
board[y][x] = 1; | |
} else if args.iter().any(|x| *x == yellow) { | |
board[y][x] = -1; | |
} else { | |
board[y][x] = 0; | |
} | |
} | |
} | |
board | |
} | |
// Drop a chip, return the new board | |
fn drop_disk(board: &Board, column:usize, color:i8) -> Board { | |
// Clone! | |
let mut board_new = board.clone(); | |
for y in (0..HEIGHT).rev() { | |
if board_new[y][column] == 0 { | |
board_new[y][column] = color; | |
break; | |
} | |
} | |
board_new | |
} | |
// TODO: Write new comment. | |
fn ab_minimax(max_or_min:bool, color:i8, depth:i32, board:&Board, debug:bool) -> (Option<usize>, i32) { | |
// Cheap, so just recalculate as needed | |
// It would be nicer to clone this, but I don't | |
// think the closure wants to let me do that. :( | |
let moves = || (0..WIDTH).filter(|&column| board[0][column] == 0); | |
// I ❤ early returns | |
if moves().next().is_none() { | |
return (None, score_board(board)); | |
} | |
// Expensive, so store in vectors. | |
let boards: Vec<_> = moves().map(|column| drop_disk(board, column, color)).collect(); | |
let scores: Vec<_> = boards.iter().map(score_board).collect(); | |
let target_score = if max_or_min { ORANGE_WINS } else { YELLOW_WINS }; | |
// Extra scope to stop killer_move stealing scores for ages, | |
// although it's not a massive deal if it is as we can just | |
// use .iter().cloned() in best_scores. | |
{ | |
let killer_move = moves().zip(&scores).find(|&(_, &score)| score == target_score); | |
if let Some((index, &score)) = killer_move { | |
return (Some(index), score); // ❤ ❤ ❤ | |
} | |
} | |
// Boxing iterators allows using multiple iterator types | |
// generically using dynamic dispatch. It's normally cheaper | |
// than storing them in vectors and tons more convenient than | |
// using an actual enum type implementing Iterator. | |
// | |
// In this case we can be even craftier by using a pointer | |
// into stack variables. We don't save much (the allocation is | |
// tiny), but it's nice to know about this option if you need it. | |
let mut shallow_iter; | |
let mut deep_iter; | |
let best_scores: &mut Iterator<Item=_> = match depth { | |
if depth == 1 { | |
shallow_iter = scores.into_iter(); | |
&mut shallow_iter | |
} | |
else { | |
// Single maps tend to be prettier, | |
// unless using predefined functions. | |
deep_iter = boards.into_iter().map(|board| { | |
let (_, best_score) = ab_minimax(!max_or_min, other_color(color), depth-1, &board, debug); | |
// when loss is certain, avoid forfeiting the match, by shifting scores by depth... | |
match best_score { | |
1000000 | -1000000 => best_score - depth*(color as i32), | |
_ => best_score | |
} | |
}); | |
&mut deep_iter | |
}; | |
// The Box method follows, for if you want something simpler but | |
// with a tiny bit more overhead | |
/* | |
// Note the use of match here, which I personally | |
// think is prettier for single-line statments | |
let best_scores: Box<Iterator<Item=_>> = match depth { | |
1 => Box::new(scores.into_iter()), | |
_ => Box::new(boards.into_iter().map(|board| { | |
... | |
})) | |
}; | |
*/ | |
let all_data = best_scores.zip(moves()) | |
.inspect(|&(column, score)| if debug && depth == MAX_DEPTH { | |
println!("Depth {}, placing on {}, Score:{}\n", depth, column, score) | |
}); | |
let best = if max_or_min { all_data.max() } else { all_data.min() }; | |
// Not sure if this is better than a match. Whatever. | |
best.map(|(score, best_move)| (Some(best_move), score)).or_else((None, 0)) | |
} | |
fn run_game() -> i32 { | |
let board = load_board(env::args().collect()); | |
let score_orig = score_board(&board); | |
let debug = env::args().any(|x| x == "-debug"); | |
if debug { | |
println!("Starting score: {}", score_orig); | |
} | |
if score_orig == ORANGE_WINS { | |
println!("I win"); | |
return -1; | |
} else if score_orig == YELLOW_WINS { | |
println!("You win"); | |
return -1; | |
} else { | |
match ab_minimax(true, 1, MAX_DEPTH, &board, debug) { | |
(Some(column), _) => println!("{}", column), | |
_ => println!("No move possible"), | |
} | |
return 0; | |
} | |
} | |
// I don't trust process::exit... :P | |
fn main() { | |
process::exit(run_game()); | |
} | |
/* vim: set expandtab ts=8 sts=4 shiftwidth=4 */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment