152 lines
5.2 KiB
Nim
152 lines
5.2 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
|
|
import std/exitprocs
|
|
|
|
|
|
template clearScreen =
|
|
eraseScreen()
|
|
setCursorPos(0, 0)
|
|
|
|
|
|
proc play(moves: Move) =
|
|
## Plays a game of tic tac toe
|
|
## against the user
|
|
clearScreen()
|
|
var game = newTicTacToe()
|
|
var moves = moves
|
|
var location: tuple[row, col: int]
|
|
var index: int
|
|
var self, enemy: TileKind
|
|
stdout.styledWrite(fgGreen, styleBright, "Wanna start first? ", fgYellow ,"[Y/n] ")
|
|
if readLine(stdin).strip(chars={'\n'}).toLowerAscii() notin ["y", "yes"]:
|
|
self = TileO
|
|
enemy = TileX
|
|
else:
|
|
self = TileX
|
|
enemy = TileO
|
|
location = where(moves.state, moves.state != game.map, 3).index(self.uint8)
|
|
game.place(self, location.row, location.col)
|
|
clearScreen()
|
|
styledEcho fgCyan, styleBright, "Computer chose ", fgYellow, $game.map.getIndex(location.row, location.col)
|
|
clearScreen()
|
|
while game.get() == Playing:
|
|
styledEcho fgBlue, styleBright, "Tic Tac Bot v1.0"
|
|
echo game, "\n"
|
|
styledEcho fgMagenta, styleBright, "You are ", fgBlue, $TileKind(enemy)
|
|
stdout.styledWrite(fgRed, styleBright, "Make your move ", fgBlue, "(", fgYellow, "1", fgGreen, "~", fgYellow, "8", fgBlue, ")", fgRed, ": ")
|
|
flushFile(stdout)
|
|
try:
|
|
index = int(parseBiggestInt(readLine(stdin).strip(chars={'\n'})))
|
|
location = ind2sub(index, game.map.shape)
|
|
dec(index)
|
|
except ValueError:
|
|
clearScreen()
|
|
styledEcho fgRed, styleBright, "Invalid move"
|
|
continue
|
|
if index notin 0..8 or TileKind(game.map[location.row, location.col]) != TileEmpty:
|
|
clearScreen()
|
|
styledEcho fgRed, styleBright, "Invalid move"
|
|
continue
|
|
game.place(enemy, location.row, location.col)
|
|
clearScreen()
|
|
if game.winner() == enemy:
|
|
echo game, "\n"
|
|
styledEcho fgGreen, styleBright, "Human wins!"
|
|
return
|
|
if game.winner() == self:
|
|
echo game, "\n"
|
|
styledEcho fgRed, styleBright, "Computer wins!"
|
|
return
|
|
if game.get() == Draw:
|
|
break
|
|
# Find best move and advance move tree
|
|
moves = moves.next[game.map.getIndex(location.row, location.col)].findBest(true, turn=self).move
|
|
location = where(moves.state, moves.state != game.map, 3).index(self.uint8)
|
|
game.place(self, location.row, location.col)
|
|
clearScreen()
|
|
if game.get() != Draw:
|
|
styledEcho fgCyan, styleBright, "Computer chose ", fgYellow, $(game.map.getIndex(location.row, location.col) + 1)
|
|
|
|
if game.winner() == enemy:
|
|
echo game, "\n"
|
|
styledEcho fgGreen, styleBright, "Human wins!"
|
|
return
|
|
if game.get() == Draw:
|
|
break
|
|
echo game
|
|
styledEcho fgYellow, styleBright, "It's a draw!"
|
|
|
|
|
|
proc hook {.noconv.} =
|
|
echo ""
|
|
quit(0)
|
|
|
|
|
|
when isMainModule:
|
|
randomize()
|
|
addExitProc(resetAttributes)
|
|
setControlCHook(hook)
|
|
if isTrueColorSupported():
|
|
enableTrueColors()
|
|
var path = getCacheDir() / "ttb"
|
|
path.createDir()
|
|
path = path / "cache.bin"
|
|
var moves: Move
|
|
# Since generating the tree is pretty expensive, we cache
|
|
# it the first time we generate it (since it's not like it changes
|
|
# anyway) so that we don't have to rebuild it every time
|
|
if not fileExists(path):
|
|
styledEcho fgCyan, styleBright, "Generating move tree"
|
|
moves = generateMoves(build(@[uint8(0), 0, 0, 0, 0, 0, 0, 0, 0]).map, TileX)
|
|
styledEcho fgCyan, styleBright, "Caching data to disk..."
|
|
var fp = open(path, fmWrite)
|
|
discard fp.writeBytes(moves.dumpBytes(), 0, 8799135)
|
|
fp.close()
|
|
else:
|
|
styledEcho fgCyan, styleBright, "Loading previously cached move tree"
|
|
var fp = open(path, fmRead)
|
|
var data: seq[byte] = @[]
|
|
for _ in 0..<8799135:
|
|
data.add(byte(0))
|
|
discard fp.readBytes(data, 0, 8799135)
|
|
moves = data.loadBytes()
|
|
fp.close()
|
|
while true:
|
|
try:
|
|
play(moves)
|
|
stdout.styledWrite(fgGreen, styleBright, "Again? ", fgYellow ,"[Y/n] ")
|
|
flushFile(stdout)
|
|
if readLine(stdin).strip(chars={'\n'}).toLowerAscii() notin ["no", "n"]:
|
|
break
|
|
except IOError:
|
|
break
|
|
except EOFError:
|
|
break
|
|
except NilAccessDefect:
|
|
stdout.styledWriteLine(fgRed, styleBright, "Segmentation fault")
|
|
break
|