├── README.rst ├── .gitignore ├── Cargo.toml ├── src ├── world │ ├── mod.rs │ ├── world.rs │ └── line_world_16.rs └── main.rs ├── reload_compile.sh └── Cargo.lock /README.rst: -------------------------------------------------------------------------------- 1 | 2048 AI in Rust 2 | =============== 3 | 4 | A very simple 2048 solver using minimax, in Rust. 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | session.vim 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "2048-rust" 3 | version = "0.1.0" 4 | authors = ["Federico Giraud "] 5 | 6 | [dependencies] 7 | rand = "0.3" 8 | time = "0.1" 9 | -------------------------------------------------------------------------------- /src/world/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod world; 2 | pub mod line_world_16; 3 | 4 | pub use world::world::{add_rand_tile, Dir, Coord, Tile, World}; 5 | pub use world::line_world_16::LineWorld16; 6 | -------------------------------------------------------------------------------- /reload_compile.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | DIRECTORY_TO_OBSERVE="src" 3 | 4 | GREEN='\033[0;32m' 5 | RED='\033[0;31m' 6 | NC='\033[0m' # No Color 7 | 8 | function wait_for_change { 9 | inotifywait -r -q \ 10 | -e modify,move,create,delete \ 11 | $DIRECTORY_TO_OBSERVE 12 | } 13 | 14 | function build { 15 | echo -e "$GREEN *** Starting build ***$NC" 16 | cargo build 17 | if [ $? -eq 0 ]; then 18 | echo -e "$GREEN *** Build complete ***$NC" 19 | else 20 | echo -e "$RED *** Build failed ***$NC" 21 | fi 22 | } 23 | 24 | build 25 | 26 | while wait_for_change; do 27 | sleep 0.25; 28 | build 29 | done 30 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "2048-rust" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", 7 | ] 8 | 9 | [[package]] 10 | name = "kernel32-sys" 11 | version = "0.2.1" 12 | source = "registry+https://github.com/rust-lang/crates.io-index" 13 | dependencies = [ 14 | "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 16 | ] 17 | 18 | [[package]] 19 | name = "libc" 20 | version = "0.2.7" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [[package]] 24 | name = "rand" 25 | version = "0.3.14" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | dependencies = [ 28 | "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 29 | ] 30 | 31 | [[package]] 32 | name = "time" 33 | version = "0.1.34" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | dependencies = [ 36 | "kernel32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 37 | "libc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", 39 | ] 40 | 41 | [[package]] 42 | name = "winapi" 43 | version = "0.2.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | 46 | [[package]] 47 | name = "winapi-build" 48 | version = "0.1.1" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | 51 | -------------------------------------------------------------------------------- /src/world/world.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | 3 | use std::fmt; 4 | 5 | #[derive(Copy, Clone, Debug)] 6 | pub enum Dir { 7 | Up, 8 | Down, 9 | Left, 10 | Right, 11 | } 12 | 13 | fn generate_new_tile_value() -> i32 { 14 | match rand::random::() % 10 { 15 | 9 => 4, 16 | _ => 2, 17 | } 18 | } 19 | 20 | pub trait Tile: Copy + Eq + fmt::Display { 21 | fn to_i32(&self) -> i32; 22 | fn from_i32(i32) -> Self; 23 | 24 | fn empty(&self) -> bool { 25 | self.to_i32() == 0 26 | } 27 | } 28 | 29 | pub trait Coord: Copy { 30 | fn to_xy(&self) -> (usize, usize); 31 | fn from_xy(usize, usize) -> Self; 32 | } 33 | 34 | // TODO: add get size 35 | pub trait World<'a>: Clone { 36 | type Tile: Tile; 37 | type Coord: Coord; 38 | type Iter: Iterator; 39 | 40 | fn new() -> Self; 41 | fn get(&self, usize, usize) -> Self::Tile; 42 | fn set(&mut self, Self::Coord, Self::Tile); 43 | fn do_move(&mut self, Dir) -> bool; 44 | fn iterate(&'a self) -> Self::Iter; 45 | 46 | fn print(&self) { 47 | println!("┌────┬────┬────┬────┐"); 48 | for y in 0..4 { 49 | for x in 0..4 { 50 | let c = self.get(x, y); 51 | if c.empty() { 52 | print!("│ "); 53 | } else { 54 | print!("│{:4}", c); 55 | } 56 | } 57 | if y < 3 { 58 | println!("│\n├────┼────┼────┼────┤"); 59 | } else { 60 | println!("│\n└────┴────┴────┴────┘"); 61 | } 62 | } 63 | } 64 | } 65 | 66 | pub fn add_rand_tile(world: &mut W) -> bool 67 | where W: for<'a> World<'a, Coord=T>, 68 | T: Coord, 69 | { 70 | let empty_tiles = world.iterate().filter(|&(_, t)| t.empty()).count(); 71 | if empty_tiles == 0 { 72 | return false; 73 | } 74 | 75 | let p = rand::random::() % empty_tiles; 76 | let (coord, _) = world.iterate().filter(|&(_, t)| t.empty()).nth(p).unwrap(); 77 | world.set(coord, Tile::from_i32(generate_new_tile_value())); 78 | 79 | return true; 80 | } 81 | -------------------------------------------------------------------------------- /src/world/line_world_16.rs: -------------------------------------------------------------------------------- 1 | use world::{Dir, Coord, Tile, World}; 2 | 3 | #[derive(Copy, Clone, Debug)] 4 | pub struct LineWorld16 { 5 | pub data: [i32; 16], 6 | } 7 | 8 | struct Iter16 { 9 | start: i32, 10 | step: i32, 11 | off: i32, 12 | count: i32, 13 | } 14 | 15 | impl Iter16 { 16 | fn new(dir: Dir, off: i32, skip: i32) -> Iter16 { 17 | match dir { 18 | Dir::Up => Iter16 { start: 0, step: 4, off: off, count: skip }, 19 | Dir::Down => Iter16 { start: 12, step: -4, off: off, count: skip }, 20 | Dir::Left => Iter16 { start: 0, step: 1, off: off * 4, count: skip }, 21 | Dir::Right => Iter16 { start: 3, step: -1, off: off * 4, count: skip }, 22 | } 23 | } 24 | } 25 | 26 | impl Iterator for Iter16 { 27 | type Item = (i32, usize); 28 | 29 | fn next(&mut self) -> Option<(i32, usize)> { 30 | if self.count < 4 { 31 | let pair = (self.count, (self.start + self.count * self.step + self.off) as usize); 32 | self.count += 1; 33 | Some(pair) 34 | } else { 35 | None 36 | } 37 | } 38 | } 39 | 40 | impl LineWorld16 { 41 | fn squash_line(&mut self, dir: Dir, line: i32) { 42 | for (n, i) in Iter16::new(dir, line, 0) { 43 | if self.data[i] != 0 { 44 | continue; 45 | } 46 | for (_, j) in Iter16::new(dir, line, n) { 47 | if self.data[j] != 0 { 48 | self.data[i] = self.data[j]; 49 | self.data[j] = 0; 50 | break; 51 | } 52 | } 53 | } 54 | } 55 | 56 | fn merge_line(&mut self, dir: Dir, line: i32) { 57 | let mut prev: Option = None; 58 | for (_, i) in Iter16::new(dir, line, 0) { 59 | match prev { 60 | Some(p) => { 61 | if self.data[p] == self.data[i] { 62 | self.data[p] = self.data[p] * 2; 63 | self.data[i] = 0; 64 | } 65 | } 66 | None => (), 67 | }; 68 | prev = Some(i); 69 | } 70 | } 71 | } 72 | 73 | struct LineWorld16Iter<'a> { 74 | world: &'a LineWorld16, 75 | count: usize, 76 | } 77 | 78 | impl<'a> Iterator for LineWorld16Iter<'a> { 79 | type Item = (usize, i32); 80 | fn next(&mut self) -> Option<(usize, i32)> { 81 | while self.count < 16 { 82 | let c = self.count; 83 | self.count += 1; 84 | return Some((c, self.world.data[c])); 85 | } 86 | None 87 | } 88 | } 89 | 90 | impl Tile for i32 { 91 | fn to_i32(&self) -> i32 { *self } 92 | fn from_i32(v: i32) -> i32 { v } 93 | } 94 | 95 | impl Coord for usize { 96 | fn from_xy(x: usize, y: usize) -> usize { 97 | y * 4 + x 98 | } 99 | 100 | fn to_xy(&self) -> (usize, usize) { 101 | (*self % 4, *self / 4) 102 | } 103 | } 104 | 105 | impl<'a> World<'a> for LineWorld16 { 106 | type Tile = i32; 107 | type Coord = usize; 108 | type Iter = LineWorld16Iter<'a>; 109 | 110 | fn new() -> LineWorld16 { 111 | LineWorld16 { data: [0; 16] } 112 | } 113 | 114 | fn get(&self, x: usize, y: usize) -> i32 { 115 | self.data[Self::Coord::from_xy(x, y)] 116 | } 117 | 118 | fn set(&mut self, n: usize, val: i32) { 119 | self.data[n] = val; 120 | } 121 | 122 | fn do_move(&mut self, dir: Dir) -> bool { 123 | let old_data = self.data.clone(); 124 | for line in 0..4 { 125 | self.squash_line(dir, line); 126 | self.merge_line(dir, line); 127 | self.squash_line(dir, line); 128 | } 129 | return old_data != self.data; 130 | } 131 | 132 | fn iterate(&'a self) -> LineWorld16Iter<'a> { 133 | LineWorld16Iter { 134 | world: self, 135 | count: 0, 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate time; 2 | 3 | mod world; 4 | 5 | use std::cmp; 6 | use time::PreciseTime; 7 | 8 | use world::{Dir, LineWorld16, Tile, World}; 9 | 10 | fn best_tile(world: &W) -> i32 11 | where W: for <'a> World<'a> 12 | { 13 | world.iterate().fold(0i32, |best, (_, v)| cmp::max(v.to_i32(), best)) 14 | } 15 | 16 | fn total(world: &W) -> i32 17 | where W: for <'a> World<'a> 18 | { 19 | world.iterate().fold(0i32, |sum, (_, v)| sum + v.to_i32()) 20 | } 21 | 22 | trait Heuristic { 23 | fn call World<'a>>(&self, &W) -> i32; 24 | fn description(&self) -> &'static str; 25 | } 26 | 27 | struct HEmpty; 28 | impl Heuristic for HEmpty { 29 | fn call World<'a>>(&self, world: &W) -> i32 { 30 | world.iterate().filter(|&(_, t)| t.empty()).count() as i32 31 | } 32 | fn description(&self) -> &'static str { 33 | "Empty count" 34 | } 35 | } 36 | 37 | struct HSum; 38 | impl Heuristic for HSum { 39 | fn description(&self) -> &'static str { 40 | "Total sum" 41 | } 42 | fn call World<'a>>(&self, world: &W) -> i32 { 43 | world.iterate().fold(0i32, |sum, (_, v)| { 44 | let n = v.to_i32(); 45 | if n != 0 { sum + n } else { sum + 256 } 46 | }) 47 | } 48 | } 49 | 50 | struct HSquared; 51 | impl Heuristic for HSquared { 52 | fn description(&self) -> &'static str { 53 | "Squared" 54 | } 55 | fn call World<'a>>(&self, world: &W) -> i32 { 56 | world.iterate().fold(0i32, |sum, (_, v)| sum + v.to_i32().pow(2)) 57 | } 58 | } 59 | 60 | fn alphabeta(w: &W, depth: i32, mut alpha: i32, mut beta: i32, max_p: bool, moved: bool, h: &F) -> (Dir, i32) 61 | where W: for<'a> World<'a>, 62 | F: Heuristic, 63 | { 64 | if depth <= 0 || !moved { 65 | return (Dir::Up, h.call(w)); 66 | } 67 | 68 | let empty_tiles = w.iterate().filter(|&(_, t)| t.empty()).count(); 69 | let new_depth = if empty_tiles > 8 { depth / 2 } else { depth }; 70 | 71 | if max_p { 72 | let mut best: (Dir, i32) = (Dir::Up, std::i32::MIN); 73 | for &d in [Dir::Up, Dir::Down, Dir::Left, Dir::Right].iter() { 74 | let ref mut w1 = w.clone(); 75 | let moved = w1.do_move(d); 76 | let (_, val) = alphabeta(w1, new_depth - 1, alpha, beta, false, moved, h); 77 | if best.1 < val { 78 | best = (d, val); 79 | } 80 | alpha = cmp::max(alpha, best.1); 81 | if beta <= alpha { 82 | break; 83 | } 84 | } 85 | return best; 86 | } else { 87 | let mut best: (Dir, i32) = (Dir::Up, std::i32::MAX); 88 | for (c, v) in w.iterate() { 89 | if !v.empty() { 90 | continue; 91 | } 92 | for &x in [2, 4].iter() { 93 | let ref mut w1 = w.clone(); 94 | w1.set(c, W::Tile::from_i32(x)); 95 | let (_, val) = alphabeta(w1, new_depth - 1, alpha, beta, true, true, h); 96 | if best.1 > val { 97 | best = (Dir::Up, val); 98 | } 99 | beta = cmp::min(beta, best.1); 100 | if beta <= alpha { 101 | break; 102 | } 103 | } 104 | } 105 | return best; 106 | } 107 | } 108 | 109 | fn main() { 110 | let mut world = LineWorld16::new(); 111 | let start_time = PreciseTime::now(); 112 | let mut count = 0; 113 | let mut last_print = -1; 114 | 115 | let h = HSquared; 116 | let depth = 9; 117 | 118 | while world::add_rand_tile::(&mut world) { 119 | let (d, v) = alphabeta(&world, depth, std::i32::MIN, std::i32::MAX, true, true, &h); 120 | world.do_move(d); 121 | count += 1; 122 | let duration_s = start_time.to(PreciseTime::now()).num_seconds(); 123 | if duration_s != last_print { 124 | last_print = duration_s; 125 | println!("> {:?} {}", d, v); 126 | world.print(); 127 | } 128 | } 129 | 130 | println!("Three depth: {}", depth); 131 | println!("Using heuristic: {}\n", h.description().to_string()); 132 | world.print(); 133 | 134 | let duration = start_time.to(PreciseTime::now()); 135 | println!("{} moves in {}", count, duration); 136 | println!("{} moves per second", count / cmp::max(1, duration.num_seconds())); 137 | 138 | println!("{} total, {} best", total(&world), best_tile(&world)); 139 | } 140 | --------------------------------------------------------------------------------