# Copyright 2022 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import board import ../util/matrix export board export matrix import std/strutils import std/segfaults import std/terminal import std/random import std/os randomize() proc play(treeA, treeB: Move) = ## Plays a game of tic tac toe ## against the user stdout.write("\x1Bc") var game = newTicTacToe() var moves = treeA var location: tuple[row, col: int] var index: int stdout.styledWrite(fgGreen, styleBright, "Wanna start first? ", fgYellow ,"[Y/n] ") if readLine(stdin).strip(chars={'\n'}).toLowerAscii() in ["n", "no"]: moves = treeB location = where(moves.state, moves.state != game.map, 3).index(Self.uint8) game.place(Self, location.row, location.col) stdout.write("\x1Bc") styledEcho fgCyan, styleBright, "Computer chose ", fgYellow, $game.map.getIndex(location.row, location.col) else: stdout.write("\x1Bc") while game.get() == Playing: styledEcho fgBlue, styleBright, "Tic Tac Bot v1.0" echo game, "\n" styledEcho fgMagenta, styleBright, "You are ", fgBlue, "O" stdout.styledWrite(fgRed, styleBright, "Make your move ", fgBlue, "(", fgYellow, "0", fgGreen, "~", fgYellow, "8", fgBlue, ")", fgRed, ": ") flushFile(stdout) try: index = int(parseBiggestInt(readLine(stdin).strip(chars={'\n'}))) location = ind2sub(index, game.map.shape) except ValueError: stdout.write("\x1Bc") styledEcho fgRed, styleBright, "Invalid move" continue if index notin 0..8 or TileKind(game.map[location.row, location.col]) != Empty: stdout.write("\x1Bc") styledEcho fgRed, styleBright, "Invalid move" continue game.place(Enemy, location.row, location.col) stdout.write("\x1Bc") if game.get() == WinO: echo game, "\n" styledEcho fgGreen, styleBright, "Human wins!" return moves = moves.find(game.map) moves = moves.findBest(game.map, true).move location = where(moves.state, moves.state != game.map, 3).index(Self.uint8) game.place(Self, location.row, location.col) stdout.write("\x1Bc") if game.get() != Draw: styledEcho fgCyan, styleBright, "Computer chose ", fgYellow, $game.map.getIndex(location.row, location.col) if game.get() == WinX: echo game, "\n" styledEcho fgRed, styleBright, "Computer wins!" return echo game echo "It's a draw!" proc hook {.noconv.} = echo "" quit(0) when isMainModule: var path = getCacheDir() / "ttb" path.createDir() path = path / "cache.bin" setControlCHook(hook) var movesA: Move var movesB: Move # Since generating two full trees is pretty expensive, we cache # them the first time we generate them (since it's not like they # change anyway) so that we don't have to regenerate them every time if not fileExists(path): styledEcho fgCyan, styleBright, "Generating move trees..." # Sadly we need to generate two trees for both cases where either we or # our opponent have the first turn, as the states between them are not # interchangeable at all (trust me, I tried) movesA = generateMoves(build(@[uint8(0), 0, 0, 0, 0, 0, 0, 0, 0]).map, Enemy) movesB = generateMoves(build(@[uint8(0), 0, 0, 0, 0, 0, 0, 0, 0]).map, Self) styledEcho fgCyan, styleBright, "Caching results to disk..." var fp = open(path, fmWrite) discard fp.writeBytes(movesA.dumpBytes(), 0, 8799135) discard fp.writeBytes(movesB.dumpBytes(), 0, 8799135) fp.close() else: styledEcho fgCyan, styleBright, "Loading previously cached move trees..." var fp = open(path, fmRead) var data: seq[byte] = @[] for _ in 0..<8799135: data.add(byte(0)) discard fp.readBytes(data, 0, 8799135) movesA = data.loadBytes() discard fp.readBytes(data, 0, 8799135) movesB = data.loadBytes() fp.close() # Here we pick one of the first 5 best moves so that the bot doesn't # always start with an X in the left corner when it's playing first var state = movesB.state.copy() var best: seq[Move] = @[] for i in 0..4: best.add(movesB.findBest(state, true, i).move) movesB = sample(best) while true: try: play(movesA, movesB) stdout.styledWrite(fgGreen, styleBright, "Again? ", fgYellow ,"[Y/n] ") flushFile(stdout) if readLine(stdin).strip(chars={'\n'}).toLowerAscii() in ["no", "n"]: break except IOError: break except EOFError: break except NilAccessDefect: stdout.styledWriteLine(fgRed, styleBright, "Segmentation fault") break