Day 10: Tic tac toe with GUI!!
Hopefully, you're done with day 9 and enjoyed playing tic tac toe.
Expectation
It's fun to play on the command line, but it'd be very cool to have some GUI with some buttons using libui bindings in Nim
- make sure to install it using
nimble install ui
Implementation
In the previous day we reached some good abstraction separating the logic for the command line gui and the minmax algorithm and it's not tightly coupled
minimal ui application
proc gui*() =
var mainwin = newWindow("tictactoe", 400, 500, true)
show(mainwin)
mainLoop()
when isMainModule:
# cli()
init()
gui()
Here we create a window 400x500 with a title tictactoe
and we show it and start its mainLoop getting ready to receive and dispatch events
TicTacToe GUI
We can imagine the gui to be something like that
---------------------------------------------
| --------------------------------------- |
+ | INFO LABEL | button to restart | +
| ---------------------------------------| |
+ |--------------------------------------| +
| | btn | btn | btn | |
+ |--------------------------------------| +
| | btn | btn | btn | |
+ |--------------------------------------| +
| | btn | btn | btn | |
+ |--------------------------------------| +
---------------------------------------------
- a window that contains a vertical box
- the vertical box contains 4 rows
- first row to show information about the current game and a button to reset the game
- and the other rows represent the 3x3 tictactoe grid that will reflect
game.list
:) - and 9 buttons to be pressed to set X or O
- we will support human vs AI so when human presses a button it gets disabled and the AI presses the button that minimizes its loss and that button gets disabled too.
proc gui*() =
var mainwin = newWindow("tictactoe", 400, 500, true)
# game object to contain the state, the players, the difficulty,...
var g = newGame(aiPlayer="O", difficulty=9)
var currentMove = -1
mainwin.margined = true
mainwin.onClosing = (proc (): bool = return true)
# set up the boxes
let box = newVerticalBox(true)
let hbox0 = newHorizontalBox(true)
let hbox1 = newHorizontalBox(true)
let hbox2 = newHorizontalBox(true)
let hbox3 = newHorizontalBox(true)
# list of buttons
var buttons = newSeq[Button]()
# information label
var labelInfo = newLabel("Info: Player X turn")
hbox0.add(labelInfo)
# restart button
hbox0.add(newButton("Restart", proc() =
g =newGame(aiPlayer="O", difficulty=9)
for i, b in buttons.pairs:
b.text = $i
b.enable()))
Here we setup the layout we just described and create a button Restart that resets the game again and restore the buttons text and enables them all
# create the buttons
for i in countup(0, 8):
var handler : proc()
closureScope:
let senderId = i
handler = proc() =
currentMove = senderId
g.board.list[senderId] = g.currentPlayer
g.change_player()
labelInfo.text = "Current player: " & g.currentPlayer
for i, v in g.board.list.pairs:
buttons[i].text = v
let (done, winner) = g.board.done()
if done == true:
echo g.board
if winner == "tie":
labelInfo.text = "Tie.."
else:
labelInfo.text = winner & " won."
else:
aiPlay()
buttons[senderId].disable()
buttons.add(newButton($i, handler))
- Here we create the buttons please notice we are using
closureScope
feature to capture the button id to keep track of which button is clicked - after pressing set set the text of the button to
X
- we disable the button so we don't receive anymore events.
- switch turns
- update the information label whether about the next player or the game state
- if the game is still going we ask the AI for a move
# code to run when the game asks the ai to play (after each move from the human..)
proc aiPlay() =
if g.currentPlayer == g.aiPlayer:
let emptySpots = g.board.emptySpots()
if len(emptySpots) <= g.difficulty:
let move = g.getBestMove(g.board, g.aiPlayer)
g.board.list[move.idx] = g.aiPlayer
buttons[move.idx].disable()
else:
let rndmove = emptyspots.rand()
g.board.list[rndmove] = g.aiPlayer
g.change_player()
labelInfo.text = "Current player: " & g.currentPlayer
for i, v in g.board.list.pairs:
buttons[i].text = v
let (done, winner) = g.board.done()
if done == true:
echo g.board
if winner == "tie":
labelInfo.text = "Tie.."
else:
labelInfo.text = winner & " won."
- using minmax algorithm from the previous day we calculate the best move
- change the button text to
O
- disable the button
- update the information label
hbox1.add(buttons[0])
hbox1.add(buttons[1])
hbox1.add(buttons[2])
hbox2.add(buttons[3])
hbox2.add(buttons[4])
hbox2.add(buttons[5])
hbox3.add(buttons[6])
hbox3.add(buttons[7])
hbox3.add(buttons[8])
box.add(hbox0, true)
box.add(hbox1, true)
box.add(hbox2, true)
box.add(hbox3, true)
mainwin.setChild(box)
- Here we add the buttons to their correct rows in the correct columns and set the main widget
show(mainwin)
mainLoop()
when isMainModule:
init()
gui()
Code is available on https://github.com/xmonader/nim-tictactoe/blob/master/src/nim_tictactoe_gui.nim