aboutsummaryrefslogtreecommitdiffstats
path: root/test/tictactoe/tictactoe_player.lua
blob: 4c9d56b529a4116c66272255d4a6ec5510557b56 (plain) (blame)
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
local insert = table.insert
local ipairs = ipairs
local remove = table.remove
local unpack = unpack

require 'curses'
local addch  = curses.addch
local getch  = curses.getch

module 'tictactoe_player'

local function get_move_human(self, board)
    -- first find the first empty tile on the board, so that we start the
    -- cursor off in an empty tile
    local y, x
    for row in ipairs(board.board) do
        for col in ipairs(board.board[row]) do
            if board:at(row, col) == nil then
                y, x = row, col
                break
            end
        end
        if y or x then break end
    end

    while true do
        local c
        -- draw the character we are about to put down under the cursor only if
        -- the tile under the cursor is empty
        if board:at(y, x) == nil then
            addch(board:position(y, x), self.symbol:upper())
            c = getch(board:position(y, x))
            addch(board:position(y, x), " ")
        else
            c = getch(board:position(y, x))
        end

            if c == "left"  or c == "h" then
            x = (x - 2) % 3 + 1
        elseif c == "right" or c == "l" then
            x = x % 3 + 1
        elseif c == "up"    or c == "k" then
            y = (y - 2) % 3 + 1
        elseif c == "down"  or c == "j" then
            y = y % 3 + 1
        elseif c == "enter" or c == " " then
            -- don't return illegal moves
            if board:at(y, x) == nil then
                return y, x
            end
        end
    end
end

-- recursive helper function for the computer's move calculation
-- it returns true if the player can certainly win from the given board
-- position and the given turn
-- it returns false if the player will lose against a perfect opponent
-- it returns nil if the player can force a draw, but cannot win
-- the second return value is a list of positions that correspond to the move
-- predictions for the rest of the game
local function find_move(self, board, turn)
    local tiles = board:empty_tiles()
    -- hardcode starting move, since there's only one best move anyway, and
    -- this speeds things up quite a bit
    if #tiles == 9 then
        return nil, {{3, 3}}
    end

    local winner = board:winner()
    if winner then
        if winner == self.symbol then return true, {}
        else return false
        end
    elseif #tiles == 0 then
        return nil, {}
    else
        local next_turn
        if turn == "x" then next_turn = "o" else next_turn = "x" end

        local won, path, tie_path
        for _, tile in ipairs(tiles) do
            -- create a copy of the board with the next position to try
            -- filled in, and see if there is a winning position
            local copy = board:clone()
            copy:mark(turn, unpack(tile))
            won, path = find_move(self, copy, next_turn)

            if self.symbol == turn then
                -- if it's our turn and the move we just tried will certainly 
                -- win, then return that move
                if won == true then
                    insert(path, tile)
                    return true, path
                end
            else
                -- if it's the opponent's turn and the opponent's move will
                -- make us lose, then drop the rest of this branch
                if won == false then return false end
            end

            -- keep track of if we found a path that will result in a tie,
            -- since that is the second best result
            if won == nil then
                insert(path, tile)
                tie_path = path
            end
        end

        -- if we haven't yet returned with a winning path, then return with
        -- a tie if possible
        if tie_path then return nil, tie_path end

        -- if we get here and it's our turn, that means that all possibilities
        -- for our move will make us lose, so drop this branch
        -- if we get here and it's the opponent's turn, that means that
        -- all possibilities for the opponent's move will make us win, so just
        -- return any path
        if self.symbol == turn then return false
        else return true, path
        end
    end
end

local function get_move_computer(self, board)
    local win, path = find_move(self, board, self.symbol)
    -- the next move is the end of the path that find_move returned
    return unpack(remove(path))
end

function new(type, symbol)
    local get_move
    if type == "human" then
        get_move = get_move_human
    else
        get_move = get_move_computer
    end

    -- we don't need an actual object, a table will do fine
    return {get_move = get_move, symbol = symbol}
end