├── .gitignore ├── src ├── strategies │ ├── mod.rs │ ├── random.rs │ └── negamax.rs ├── lib.rs ├── util.rs └── interface.rs ├── .travis.yml ├── .project ├── Cargo.toml ├── LICENSE ├── tests └── ttt.rs ├── benches └── negamax.rs ├── README.md └── examples └── ttt.rs /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /src/strategies/mod.rs: -------------------------------------------------------------------------------- 1 | //! Strategy implementations. 2 | 3 | pub mod negamax; 4 | pub mod random; 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate rand; 2 | 3 | pub mod interface; 4 | pub mod strategies; 5 | pub mod util; 6 | 7 | pub use interface::{Evaluation, Evaluator, Game, Move, Player, Strategy, Winner}; 8 | pub use strategies::negamax::{Negamax, Options}; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | script: 11 | - cargo clean 12 | - cargo build 13 | - cargo package 14 | - cargo test 15 | - cargo doc 16 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | minimax 4 | 5 | 6 | 7 | 8 | 9 | com.github.rustdt.ide.core.Builder 10 | 11 | 12 | 13 | 14 | 15 | com.github.rustdt.ide.core.nature 16 | 17 | 18 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "minimax" 3 | version = "0.0.2" 4 | authors = [ "Samuel Fredrickson " ] 5 | description = "Generic implementations of Minimax." 6 | documentation = "http://kinghajj.github.io/doc/minimax/" 7 | repository = "http://github.com/kinghajj/minimax-rs.git" 8 | readme = "README.md" 9 | keywords = ["ai", "game", "minimax", "negamax"] 10 | license = "MIT" 11 | 12 | [dependencies] 13 | rand = "0.3.*" 14 | 15 | [profile.test] 16 | opt-level = 3 17 | -------------------------------------------------------------------------------- /src/strategies/random.rs: -------------------------------------------------------------------------------- 1 | //! A strategy that randomly chooses a move, for use in tests. 2 | 3 | use super::super::interface::*; 4 | use rand; 5 | use rand::Rng; 6 | 7 | pub struct Random { 8 | rng: rand::ThreadRng, 9 | } 10 | 11 | impl Random { 12 | pub fn new() -> Random { 13 | Random { rng: rand::thread_rng() } 14 | } 15 | } 16 | 17 | impl Strategy for Random 18 | where G::M: Copy { 19 | fn choose_move(&mut self, s: &G::S, p: Player) -> Option { 20 | let mut moves: [Option; 100] = [None; 100]; 21 | match G::generate_moves(s, p, &mut moves) { 22 | 0 => None, 23 | num_moves => Some(moves[self.rng.gen_range(0, num_moves)].unwrap()), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Samuel Fredrickson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for testing, and tests. 2 | 3 | use super::interface; 4 | use super::interface::Move; 5 | use std::default::Default; 6 | 7 | /// Play a complete, new game with players using the two provided strategies. 8 | /// 9 | /// The first strategy will be `Player::Computer`, the other `Player::Opponent`. 10 | /// Returns result of the game. 11 | pub fn battle_royale(s1: &mut S1, s2: &mut S2) -> interface::Winner 12 | where G: interface::Game, 13 | G::S: Default, 14 | S1: interface::Strategy, 15 | S2: interface::Strategy 16 | { 17 | let mut state = G::S::default(); 18 | let mut strategies: Vec<(interface::Player, &mut interface::Strategy)> = vec![ 19 | (interface::Player::Computer, s1), 20 | (interface::Player::Opponent, s2), 21 | ]; 22 | let mut s = 0; 23 | while G::get_winner(&state).is_none() { 24 | let (p, ref mut strategy) = strategies[s]; 25 | match strategy.choose_move(&mut state, p) { 26 | Some(m) => m.apply(&mut state), 27 | None => break, 28 | } 29 | s = 1 - s; 30 | } 31 | G::get_winner(&state).unwrap() 32 | } 33 | -------------------------------------------------------------------------------- /tests/ttt.rs: -------------------------------------------------------------------------------- 1 | extern crate minimax; 2 | 3 | #[path="../examples/ttt.rs"] 4 | mod ttt; 5 | 6 | use minimax::util::battle_royale; 7 | 8 | // Ensure that two players using negamax always results in a draw. 9 | #[test] 10 | fn test_ttt_negamax_always_draws() { 11 | use minimax::strategies::negamax::{Negamax, Options}; 12 | let mut s1 = Negamax::::new(Options { max_depth: 10 }); 13 | let mut s2 = Negamax::::new(Options { max_depth: 10 }); 14 | for _ in 0..100 { 15 | assert!(battle_royale(&mut s1, &mut s2) == minimax::Winner::Draw) 16 | } 17 | } 18 | 19 | // Ensure that a player using negamax against a random one always results in 20 | // either a draw or a win for the former player. 21 | #[test] 22 | fn test_ttt_negamax_vs_random_always_wins_or_draws() { 23 | use minimax::strategies::negamax::{Negamax, Options}; 24 | use minimax::strategies::random::Random; 25 | let mut s1 = Negamax::::new(Options { max_depth: 10 }); 26 | let mut s2 = Random::new(); 27 | for _ in 0..100 { 28 | assert!(battle_royale(&mut s1, &mut s2) != 29 | minimax::Winner::Competitor(minimax::Player::Opponent)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /benches/negamax.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate minimax; 3 | extern crate test; 4 | use test::Bencher; 5 | use minimax::*; 6 | 7 | #[derive(Clone)] 8 | pub struct Board; 9 | 10 | #[derive(Copy, Clone)] 11 | pub struct Place; 12 | 13 | pub struct Eval; 14 | 15 | pub struct Noop; 16 | 17 | impl Move for Place { 18 | type G = Noop; 19 | fn apply(&self, _: &mut Board) { 20 | } 21 | fn undo(&self, _: &mut Board) { 22 | } 23 | } 24 | 25 | impl Game for Noop { 26 | type S = Board; 27 | type M = Place; 28 | 29 | fn generate_moves(_: &Board, _: Player, ms: &mut [Option]) -> usize { 30 | const NUM_MOVES: usize = 4; 31 | for m in ms.iter_mut().take(NUM_MOVES) { 32 | *m = Some(Place); 33 | } 34 | ms[NUM_MOVES] = None; 35 | NUM_MOVES 36 | } 37 | 38 | fn get_winner(_: &Board) -> Option { 39 | None 40 | } 41 | } 42 | 43 | impl Evaluator for Eval { 44 | type G = Noop; 45 | 46 | fn evaluate(_: &Board, _: Option) -> Evaluation { 47 | Evaluation::Score(0) 48 | } 49 | } 50 | 51 | #[bench] 52 | fn bench_negamax(b: &mut Bencher) { 53 | let board = Board; 54 | let mut s = Negamax::::new(Options { max_depth: 10 }); 55 | b.iter(|| s.choose_move(&board, Player::Computer)); 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minimax-rs - Generic implementations of Minimax in Rust. 2 | 3 | [![Build Status](https://travis-ci.org/kinghajj/minimax-rs.svg?branch=master)](https://travis-ci.org/kinghajj/minimax-rs) [![Crates.io](https://img.shields.io/crates/v/minimax.svg)](https://crates.io/crates/minimax) 4 | 5 | ## About 6 | 7 | This library provides interfaces that describe: 8 | 9 | 1. the rules for two-player, perfect-knowledge games; 10 | 2. methods of evaluating particular game states for a player; and 11 | 3. strategies for choosing moves for a player. 12 | 13 | The eventual goal is to have multiple proper strategies, so that any combination 14 | of evaluators and strategies can be tested against each other. Currently, only 15 | a basic alpha-beta pruning Negamax strategy is implemented. 16 | 17 | ## Example 18 | 19 | The `ttt` module contains an implementation of Tic-Tac-Toe, demonstrating how to 20 | use the game and evaluation interfaces. `test` shows how to use strategies. 21 | 22 | ## License 23 | 24 | Copyright (c) 2015 Samuel Fredrickson 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy 27 | of this software and associated documentation files (the "Software"), to deal 28 | in the Software without restriction, including without limitation the rights 29 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | copies of the Software, and to permit persons to whom the Software is 31 | furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in 34 | all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 42 | THE SOFTWARE. 43 | -------------------------------------------------------------------------------- /src/strategies/negamax.rs: -------------------------------------------------------------------------------- 1 | //! An implementation of Negamax. 2 | //! 3 | //! Currently, only the basic alpha-pruning variant is implemented. Further work 4 | //! could add advanced features, like history and/or transposition tables. This 5 | //! picks randomly among the "best" moves, so that it's non-deterministic. 6 | 7 | use super::super::interface::*; 8 | use rand; 9 | use rand::Rng; 10 | use std::cmp::max; 11 | use std::marker::PhantomData; 12 | 13 | fn negamax(s: &mut ::S, 14 | depth: usize, 15 | mut alpha: Evaluation, 16 | beta: Evaluation, 17 | p: Player) 18 | -> Evaluation 19 | where <::G as Game>::M: Copy 20 | { 21 | let maybe_winner = E::G::get_winner(s); 22 | if depth == 0 || maybe_winner.is_some() { 23 | return p * E::evaluate(s, maybe_winner); 24 | } 25 | let mut moves = [None; 100]; 26 | E::G::generate_moves(s, p, &mut moves); 27 | let mut best = Evaluation::Worst; 28 | for m in moves.iter().take_while(|om| om.is_some()).map(|om| om.unwrap()) { 29 | m.apply(s); 30 | let value = -negamax::(s, depth - 1, -beta, -alpha, -p); 31 | m.undo(s); 32 | best = max(best, value); 33 | alpha = max(alpha, value); 34 | if alpha >= beta { 35 | break 36 | } 37 | } 38 | best 39 | } 40 | 41 | /// Options to use for the `Negamax` engine. 42 | pub struct Options { 43 | /// The maximum depth within the game tree. 44 | pub max_depth: usize, 45 | } 46 | 47 | pub struct Negamax { 48 | opts: Options, 49 | rng: rand::ThreadRng, 50 | _eval: PhantomData, 51 | } 52 | 53 | impl Negamax { 54 | pub fn new(opts: Options) -> Negamax { 55 | Negamax { 56 | opts: opts, 57 | rng: rand::thread_rng(), 58 | _eval: PhantomData, 59 | } 60 | } 61 | } 62 | 63 | impl Strategy for Negamax 64 | where ::S: Clone, 65 | ::M: Copy { 66 | fn choose_move(&mut self, s: &::S, p: Player) -> Option<::M> { 67 | let mut best = Evaluation::Worst; 68 | let mut moves = [None; 100]; 69 | E::G::generate_moves(s, p, &mut moves); 70 | let mut candidate_moves = Vec::new(); 71 | let mut s_clone = s.clone(); 72 | for m in moves.iter().take_while(|m| m.is_some()).map(|m| m.unwrap()) { 73 | // determine value for this move 74 | m.apply(&mut s_clone); 75 | let value = -negamax::(&mut s_clone, 76 | self.opts.max_depth, 77 | Evaluation::Worst, 78 | Evaluation::Best, 79 | -p); 80 | m.undo(&mut s_clone); 81 | // this move is a candidate move 82 | if value == best { 83 | candidate_moves.push(m); 84 | // this move is better than any previous, so it's the sole candidate 85 | } else if value > best { 86 | candidate_moves.clear(); 87 | candidate_moves.push(m); 88 | best = value; 89 | } 90 | } 91 | if candidate_moves.is_empty() { 92 | None 93 | } else { 94 | Some(candidate_moves[self.rng.gen_range(0, candidate_moves.len())]) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/interface.rs: -------------------------------------------------------------------------------- 1 | //! The common structures and traits. 2 | 3 | use std::ops; 4 | 5 | /// A competitor within a game. 6 | /// 7 | /// For simplicity, only two players are supported. Their values correspond to 8 | /// the "color" parameter in Negamax. 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 10 | #[repr(i8)] 11 | pub enum Player { 12 | Computer = 1, 13 | Opponent = -1, 14 | } 15 | 16 | /// Negating a player results in the opposite one. 17 | impl ops::Neg for Player { 18 | type Output = Player; 19 | #[inline] 20 | fn neg(self) -> Player { 21 | match self { 22 | Player::Computer => Player::Opponent, 23 | Player::Opponent => Player::Computer, 24 | } 25 | } 26 | } 27 | 28 | /// An assessment of a game state from a particular player's perspective. 29 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 30 | pub enum Evaluation { 31 | /// An absolutely disastrous outcome, e.g. a loss. 32 | Worst, 33 | /// An outcome with some score. Higher values mean a more favorable state. 34 | Score(i64), 35 | /// An absolutely wonderful outcome, e.g. a win. 36 | Best, 37 | } 38 | 39 | /// Negating an evaluation results in the corresponding one from the other 40 | /// player's persective. 41 | impl ops::Neg for Evaluation { 42 | type Output = Evaluation; 43 | #[inline] 44 | fn neg(self) -> Evaluation { 45 | match self { 46 | Evaluation::Worst => Evaluation::Best, 47 | Evaluation::Score(s) => Evaluation::Score(-s), 48 | Evaluation::Best => Evaluation::Worst, 49 | } 50 | } 51 | } 52 | 53 | /// Multiplying a player and an evaluation negates the latter iff the former 54 | /// is `Opponent`. 55 | impl ops::Mul for Player { 56 | type Output = Evaluation; 57 | #[inline] 58 | fn mul(self, e: Evaluation) -> Evaluation { 59 | match self { 60 | Player::Computer => e, 61 | Player::Opponent => -e, 62 | } 63 | } 64 | } 65 | 66 | /// Evaluates a game's positions. 67 | /// 68 | /// The methods are defined recursively, so that implementing one is sufficient. 69 | pub trait Evaluator { 70 | /// The type of game that can be evaluated. 71 | type G: Game; 72 | /// Evaluate the state from the persective of `Player::Computer`. 73 | #[inline] 74 | fn evaluate(s: &::S, mw: Option) -> Evaluation { 75 | Self::evaluate_for(s, mw, Player::Computer) 76 | } 77 | 78 | /// Evaluate the state from the given player's persective. 79 | #[inline] 80 | fn evaluate_for(s: &::S, mw: Option, p: Player) -> Evaluation { 81 | p * Self::evaluate(s, mw) 82 | } 83 | } 84 | 85 | /// Defines how a move affects the game state. 86 | /// 87 | /// A move is able to change initial `Game` state, as well as revert the state. 88 | /// This allows the game tree to be searched with a constant amount of space. 89 | pub trait Move { 90 | /// The type of game that the move affects. 91 | type G: Game; 92 | /// Change the state of `S` so that the move is applied. 93 | #[inline] 94 | fn apply(&self, &mut ::S); 95 | /// Revert the state of `S` so that the move is undone. 96 | #[inline] 97 | fn undo(&self, &mut ::S); 98 | } 99 | 100 | /// The result of playing a game until it finishes. 101 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 102 | pub enum Winner { 103 | /// A player won. 104 | Competitor(Player), 105 | /// Nobody won. 106 | Draw, 107 | } 108 | 109 | /// Defines the rules for a two-player, perfect-knowledge game. 110 | /// 111 | /// A game ties together types for the state and moves, generates the possible 112 | /// moves from a particular state, and determines whether a state is terminal. 113 | pub trait Game : Sized { 114 | /// The type of the game state. 115 | type S; 116 | /// The type of game moves. 117 | type M: Move; 118 | 119 | /// Generate moves for a player at the given state. After finishing, the 120 | /// next entry in the slice should be set to `None` to indicate the end. 121 | /// Returns the number of moves generated. 122 | /// 123 | /// Currently, there's a deficiency that all strategies assume that at most 124 | /// 100 moves may be generated for any position, which allows the underlying 125 | /// memory for the slice to be a stack-allocated array. One stable, this 126 | /// trait will be extended with an associated constant to specify the 127 | /// maximum number of moves. 128 | #[inline] 129 | fn generate_moves(&Self::S, Player, &mut [Option]) -> usize; 130 | 131 | /// Returns `Some(Competitor(winning_player))` if there's a winner, 132 | /// `Some(Draw)` if the state is terminal without a winner, and `None` if 133 | /// the state is non-terminal. 134 | fn get_winner(&Self::S) -> Option; 135 | } 136 | 137 | /// Defines a method of choosing a move for either player in a any game. 138 | pub trait Strategy { 139 | fn choose_move(&mut self, &G::S, Player) -> Option; 140 | } 141 | -------------------------------------------------------------------------------- /examples/ttt.rs: -------------------------------------------------------------------------------- 1 | //! A definition of the game Tic-Tac-Toe using the library, for use in tests. 2 | //! 3 | //! For example, playing a correctly-implemented strategy against itself should 4 | //! always result in a draw; and playing such a strategy against one that picks 5 | //! moves randomly should always result in a win or draw. 6 | #![allow(dead_code)] 7 | 8 | extern crate minimax; 9 | 10 | use std::default::Default; 11 | use std::fmt::{Display, Formatter, Result}; 12 | use std::convert::From; 13 | 14 | #[derive(Copy, Clone, PartialEq, Eq)] 15 | #[repr(u8)] 16 | pub enum Square { 17 | Empty, 18 | X, 19 | O, 20 | } 21 | 22 | impl Default for Square { 23 | fn default() -> Square { 24 | Square::Empty 25 | } 26 | } 27 | 28 | impl Display for Square { 29 | fn fmt(&self, f: &mut Formatter) -> Result { 30 | write!(f, 31 | "{}", 32 | match *self { 33 | Square::Empty => ' ', 34 | Square::X => 'X', 35 | Square::O => 'O', 36 | }) 37 | } 38 | } 39 | 40 | impl From for Square { 41 | fn from(p: minimax::Player) -> Square { 42 | match p { 43 | minimax::Player::Computer => Square::X, 44 | minimax::Player::Opponent => Square::O, 45 | } 46 | } 47 | } 48 | 49 | impl From for minimax::Player { 50 | fn from(s: Square) -> minimax::Player { 51 | match s { 52 | Square::X => minimax::Player::Computer, 53 | Square::O => minimax::Player::Opponent, 54 | _ => panic!("From::from(Square::Empty))"), 55 | } 56 | } 57 | } 58 | 59 | #[derive(Clone, PartialEq, Eq)] 60 | pub struct Board { 61 | squares: [Square; 9], 62 | } 63 | 64 | impl Default for Board { 65 | fn default() -> Board { 66 | Board { squares: [Square::default(); 9] } 67 | } 68 | } 69 | 70 | impl Display for Board { 71 | fn fmt(&self, f: &mut Formatter) -> Result { 72 | try!(writeln!(f, 73 | "{} | {} | {}", 74 | self.squares[0], 75 | self.squares[1], 76 | self.squares[2])); 77 | try!(writeln!(f, 78 | "{} | {} | {}", 79 | self.squares[3], 80 | self.squares[4], 81 | self.squares[5])); 82 | try!(writeln!(f, 83 | "{} | {} | {}", 84 | self.squares[6], 85 | self.squares[7], 86 | self.squares[8])); 87 | Ok(()) 88 | } 89 | } 90 | 91 | pub struct Game; 92 | 93 | impl minimax::Game for Game { 94 | type S = Board; 95 | type M = Place; 96 | 97 | fn generate_moves(b: &Board, p: minimax::Player, ms: &mut [Option]) -> usize { 98 | let mut j = 0; 99 | for i in 0..b.squares.len() { 100 | if b.squares[i] == Square::Empty { 101 | ms[j] = Some(Place { 102 | i: i as u8, 103 | s: From::from(p), 104 | }); 105 | j += 1; 106 | } 107 | } 108 | ms[j] = None; 109 | j 110 | } 111 | 112 | fn get_winner(b: &Board) -> Option { 113 | // horizontal wins 114 | if b.squares[0] != Square::Empty && b.squares[0] == b.squares[1] && 115 | b.squares[1] == b.squares[2] { 116 | return Some(minimax::Winner::Competitor(From::from(b.squares[0]))); 117 | } 118 | if b.squares[3] != Square::Empty && b.squares[3] == b.squares[4] && 119 | b.squares[4] == b.squares[5] { 120 | return Some(minimax::Winner::Competitor(From::from(b.squares[3]))); 121 | } 122 | if b.squares[6] != Square::Empty && b.squares[6] == b.squares[7] && 123 | b.squares[7] == b.squares[8] { 124 | return Some(minimax::Winner::Competitor(From::from(b.squares[6]))); 125 | } 126 | // vertical wins 127 | if b.squares[0] != Square::Empty && b.squares[0] == b.squares[3] && 128 | b.squares[3] == b.squares[6] { 129 | return Some(minimax::Winner::Competitor(From::from(b.squares[0]))); 130 | } 131 | if b.squares[1] != Square::Empty && b.squares[1] == b.squares[4] && 132 | b.squares[4] == b.squares[7] { 133 | return Some(minimax::Winner::Competitor(From::from(b.squares[1]))); 134 | } 135 | if b.squares[2] != Square::Empty && b.squares[2] == b.squares[5] && 136 | b.squares[5] == b.squares[8] { 137 | return Some(minimax::Winner::Competitor(From::from(b.squares[2]))); 138 | } 139 | // diagonal wins 140 | if b.squares[0] != Square::Empty && b.squares[0] == b.squares[4] && 141 | b.squares[4] == b.squares[8] { 142 | return Some(minimax::Winner::Competitor(From::from(b.squares[0]))); 143 | } 144 | if b.squares[2] != Square::Empty && b.squares[2] == b.squares[4] && 145 | b.squares[4] == b.squares[6] { 146 | return Some(minimax::Winner::Competitor(From::from(b.squares[2]))); 147 | } 148 | // draws 149 | if b.squares.iter().all(|s| *s != Square::Empty) { 150 | Some(minimax::Winner::Draw) 151 | } else { 152 | // non-terminal state 153 | None 154 | } 155 | } 156 | } 157 | 158 | #[derive(Copy, Clone, PartialEq, Eq)] 159 | pub struct Place { 160 | i: u8, 161 | s: Square, 162 | } 163 | 164 | impl Display for Place { 165 | fn fmt(&self, f: &mut Formatter) -> Result { 166 | write!(f, "{}@{}", self.s, self.i) 167 | } 168 | } 169 | 170 | impl minimax::Move for Place { 171 | type G = Game; 172 | fn apply(&self, b: &mut Board) { 173 | b.squares[self.i as usize] = self.s; 174 | } 175 | fn undo(&self, b: &mut Board) { 176 | b.squares[self.i as usize] = Square::Empty; 177 | } 178 | } 179 | 180 | pub struct Evaluator; 181 | 182 | impl minimax::Evaluator for Evaluator { 183 | type G = Game; 184 | // adapted from http://www.cs.olemiss.edu/~dwilkins/CSCI531/tic.c 185 | fn evaluate(b: &Board, mw: Option) -> minimax::Evaluation { 186 | match mw { 187 | Some(minimax::Winner::Competitor(wp)) => match wp { 188 | minimax::Player::Computer => return minimax::Evaluation::Best, 189 | minimax::Player::Opponent => return minimax::Evaluation::Worst, 190 | }, 191 | _ => {} 192 | } 193 | let mut score = 0; 194 | 195 | // 3rd: check for doubles 196 | for i in 0..3 { 197 | let line = i * 3; 198 | if b.squares[line + 0] == b.squares[line + 1] { 199 | if b.squares[line + 0] == Square::X { 200 | score += 5; 201 | } else if b.squares[line + 0] == Square::O { 202 | score -= 5; 203 | } 204 | } 205 | if b.squares[line + 1] == b.squares[line + 2] { 206 | if b.squares[line + 1] == Square::X { 207 | score += 5; 208 | } else if b.squares[line + 1] == Square::O { 209 | score += 5; 210 | } 211 | } 212 | if b.squares[i] == b.squares[3 + i] { 213 | if b.squares[i] == Square::X { 214 | score += 5; 215 | } else if b.squares[i] == Square::O { 216 | score -= 5; 217 | } 218 | } 219 | if b.squares[3 + i] == b.squares[6 + i] { 220 | if b.squares[3 + i] == Square::X { 221 | score += 5; 222 | } else if b.squares[3 + i] == Square::O { 223 | score -= 5; 224 | } 225 | } 226 | } 227 | // 2nd: check for the middle square 228 | if b.squares[4] == Square::X { 229 | score += 5; 230 | } 231 | if b.squares[4] == Square::O { 232 | score -= 5; 233 | } 234 | minimax::Evaluation::Score(score) 235 | } 236 | } 237 | 238 | fn main() { 239 | use minimax::{Game, Move, Strategy}; 240 | use minimax::strategies::negamax::{Negamax, Options}; 241 | 242 | let mut b = Board::default(); 243 | let mut strategies = vec![ 244 | (minimax::Player::Computer, Negamax::::new(Options { max_depth: 10 })), 245 | (minimax::Player::Opponent, Negamax::::new(Options { max_depth: 10 })), 246 | ]; 247 | let mut s = 0; 248 | while self::Game::get_winner(&b).is_none() { 249 | println!("{}", b); 250 | let (p, ref mut strategy) = strategies[s]; 251 | match strategy.choose_move(&mut b, p) { 252 | Some(m) => m.apply(&mut b), 253 | None => break, 254 | } 255 | s = 1 - s; 256 | } 257 | println!("{}", b); 258 | } 259 | --------------------------------------------------------------------------------