CPG/src/TicTacToe/player.nim

146 lines
5.4 KiB
Nim

# 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