summaryrefslogtreecommitdiff
path: root/tic_tac_toe/src/ai.rs
blob: 4071ee385b8a49f272b99cdbe9d7e64bce442aac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
use crate::board::{Board, Coord, Tile};
use crate::{GameState, check_state};

#[derive(Debug)]
pub struct BestMove {
    pub coord: Coord,
    eval: i8,
}

fn eval_for_player(board: &Board, player: Tile) -> i8 {
    match player {
        Tile::PlayerOne => {
            return match check_state(board) {
                GameState::PlayerTwoWin => -100,
                GameState::Draw => -10,
                GameState::InProgress => 0,
                GameState::PlayerOneWin => 100,
            };
        }
        Tile::PlayerTwo => {
            return match check_state(board) {
                GameState::PlayerOneWin => -100,
                GameState::Draw => -10,
                GameState::InProgress => 0,
                GameState::PlayerTwoWin => 100,
            };
        }
        Tile::Unowned => {
            return match check_state(board) {
                GameState::PlayerOneWin => -100,
                GameState::PlayerTwoWin => -100,
                GameState::InProgress => 0,
                GameState::Draw => 100,
            };
        }
    }
}

pub fn get_best_move(board: &Board, player: Tile, is_tl: bool) -> BestMove {
    //base case
    //game is over, return eval
    let eval = eval_for_player(board, player);

    //eval 0 means the game is in progress
    if eval != 0 {
        return BestMove {
            coord: (99, 99),
            eval,
        };
    }

    //get all possible moves
    let possible_moves: Vec<Coord> = (0..9)
        .map(|i| {
            return (i % 3, i / 3);
        })
        .filter(|coord| {
            return board.get_at_coord(*coord) == Tile::Unowned;
        })
        .collect();

    let mut cur_best = BestMove {
        eval: i8::min_value(),
        coord: possible_moves[0],
    };

    let p_move_count = possible_moves.len();

    for p_move in possible_moves {
        let mut new_board = board.clone();
        new_board.set_at_coord(p_move, player);

        let other_player = match player {
            Tile::PlayerOne => Tile::PlayerTwo,
            Tile::PlayerTwo => Tile::PlayerOne,
            //this allows the AI to set as player 'unowned', which will cause it to try to draw
            //todo: fix this
            Tile::Unowned => Tile::PlayerOne,
        };

        let response_move = get_best_move(&new_board, other_player, false);
        if response_move.coord.0 != 99 {
            new_board.set_at_coord(response_move.coord, other_player);
        }

        //this is the inverse of the response eval
        let response_eval = -response_move.eval;

        //slightly bias for the centre to beat weak players
        let centre_bias = if p_move == (1, 1) { 1 } else { 0 };

        if response_eval > cur_best.eval {
            cur_best = BestMove {
                coord: p_move,
                eval: response_eval + centre_bias,
            };
        }
    }

    if is_tl {
        //log thought process
        println!("AI > There are {p_move_count} moves that I can play");
        let sentiment = match cur_best.eval {
            -100 | -101 => "I'll lose this game if my opponent plays right",
            -9 | -10 | -11 => "If my opponent plays well, this will be a draw",
            100 | 101 => "I'll win this game",
            _ => "I can't predict the outcome of this game",
        };
        println!("AI > ({sentiment})");
        println!("AI > I'll play {cur_best:?}")
    }

    return cur_best;
}