146 lines
5.4 KiB
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
|