Skip to content

Commit

Permalink
Add nim version
Browse files Browse the repository at this point in the history
  • Loading branch information
JanEricNitschke committed Apr 19, 2024
1 parent 37be78e commit 6c8eef4
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 9 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/analog_polarization_WZ.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This workflow does stuff for the analog version

name: analog_polarization_WZ

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tictactoe_analog_polarization_WZ
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Check pictures
run: |
ls -s ./couch.png
ls -s ./window.png
28 changes: 28 additions & 0 deletions .github/workflows/nim.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# This workflow will do stuff for the nim version.

name: nim

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./tictactoe_nim
steps:
- name: Checkout repo
uses: actions/checkout@v4
- uses: jiro4989/setup-nim-action@v1
with:
nim-version: 'stable'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Compile, test and run.
run: |
nimble build
nimble test
./tictactoe_nim --X=4 --O=4
4 changes: 2 additions & 2 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ jobs:
- name: Lint with ruff
uses: chartboost/ruff-action@v1
with:
version: 0.3.4
version: 0.3.7
- name: Typecheck with pyright
uses: jakebailey/pyright-action@v2
with:
version: 1.1.356
version: 1.1.358
working-directory: ./tictactoe_python
- name: Thorough check with pylint
run: pylint tictactoe/
Expand Down
35 changes: 29 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ exclude: docs/|fibonacci/
repos:
# General
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: check-yaml
language: python
Expand All @@ -28,7 +28,7 @@ repos:
- id: check-builtin-literals
language: python
- repo: https://github.com/crate-ci/typos
rev: v1.19.0
rev: v1.20.8
hooks:
- id: typos
args: []
Expand All @@ -40,7 +40,7 @@ repos:
language: python
args: ["--write", "--check", "--output-style=google"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.3.4
rev: v0.3.7
hooks:
- id: ruff
args:
Expand Down Expand Up @@ -157,7 +157,7 @@ repos:

# javascript specific
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.0.0-rc.0
rev: v9.0.0
hooks:
- id: eslint
files: ^tictactoe_javascript/
Expand All @@ -182,8 +182,6 @@ repos:
hooks:
- id: csslint

# lua specific lints
# NONE

# ruby specific
- repo: https://github.com/rubocop/rubocop
Expand All @@ -200,6 +198,31 @@ repos:
language: system
types: [php]

# Nim specific
- repo: https://github.com/juancarlospaco/pre-commit-nim
rev: "f3e8249a482c72d02654e2babaa733782bca6175"
hooks:
- id: nim-check


- repo: local
hooks:
- id: nimpretty
name: nimpretty
entry: nimpretty
language: system
types: [nim]
- id: nimble-check
name: nimble check
entry: bash -c 'cd tictactoe_nim && nimble check .'
language: system
types: [nim]
pass_filenames: false


# lua specific lints
# NONE


# swift specific
# NONE
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,17 @@ phpunit --testdox tests
php tests/runTicTacToe.php -X 4 -O 4
```

## TicTacToe-Nim

Version using [Nim](https://nim-lang.org/).

To compile, test and run:
```
nimble build
nimble test
./tictactoe_nim --X=4 --O=4
```

## TicTacToe-scratch
Very simple two player tictactoe game with Scratch.

Expand Down
47 changes: 47 additions & 0 deletions tictactoe_nim/src/tictactoe_nim.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

from std/strutils import parseInt
from std/random import randomize
from std/parseopt import getopt


from tictactoe_nimpkg/lib import play_game

const
Usage = "TicTacToe-Nim " & """
(c) 2024 Jan-Eric Nitschke
Usage:
tictactoe [options]
Options:
--X:N[=0] Set strength for AI playing 'X' (default: No AI)
--O:N[=0] Set strength for AI playing 'O' (default: No AI)
--help show this help
"""

proc writeHelp() =
stdout.write(Usage)
stdout.flushFile()
quit(0)


when isMainModule:
randomize()
var X, O = 0
for kind, key, val in getopt():
case kind
of cmdArgument: writeHelp()
of cmdLongOption, cmdShortOption:
case key
of "X":
try:
X = parseInt(val)
except ValueError:
writeHelp()
of "O":
try:
O = parseInt(val)
except ValueError:
writeHelp()
else: writeHelp()
of cmdEnd: assert(false) # cannot happen
play_game(X, O)
183 changes: 183 additions & 0 deletions tictactoe_nim/src/tictactoe_nimpkg/lib.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
from std/strutils import parseInt
from std/os import sleep
from std/random import sample
from std/options import Option, isSome, get, none, some

type
Board = array[1..9, char]

type
Score = enum
LOSE
DRAW
WIN
Move = object
spot: range[1..9]
score: Score

proc `-`(score: Score): Score =
case score
of Score.LOSE: Score.WIN
of Score.WIN: Score.LOSE
else: score

const
WINNING_COMBINATIONS: array[1..8, array[1..3, int]] = [
[1, 2, 3], [4, 5, 6], [7, 8, 9], # Rows
[1, 4, 7], [2, 5, 8], [3, 6, 9], # Cols
[1, 5, 9], [3, 5, 7] # Diagonals
]

proc show_board(board: Board) =
echo $board[1], " | ", $board[2], " | ", $board[3]
echo "---------"
echo $board[4], " | ", $board[5], " | ", $board[6]
echo "---------"
echo $board[7], " | ", $board[8], " | ", $board[9]

proc is_winner(player: char, board: Board): bool =
for i in WINNING_COMBINATIONS:
if board[i[1]] == player and board[i[2]] == player and board[i[3]] == player:
return true
return false

proc is_full(board: Board): bool =
for cell in board:
if cell != 'X' and cell != 'O':
return false
return true

proc player_turn(player: char, board: var Board) =
var move: int
while true:
echo "Player ", player, ", enter your move (1-9): "
show_board(board)
try:
move = parseInt(readLine(stdin))
except ValueError:
echo "Invalid input. Please enter a number between 1 and 9!"
continue
if move < 1 or move > 9:
echo "Input out of range. Please enter a number between 0 and 8!"
continue
if board[move] == 'X' or board[move] == 'O':
echo "Cell already taken. Please choose another cell!"
continue
break
board[move] = player

proc swap_player(player: char): char =
if player == 'X': 'O' else: 'X'

proc get_empty_spots(board: Board): seq[int] =
var empty_spots: seq[int] = @[]
for i, cell in board:
if cell != 'X' and cell != 'O':
empty_spots.add(i)
return empty_spots

proc random_spot(board: Board): int =
return sample(get_empty_spots(board))

proc try_winning_spot(player: char, board: Board): Option[int] =
for combination in WINNING_COMBINATIONS:
var
open_spot: Option[int] = none(int)
taken_spots: int = 0
for cell in combination:
if board[cell] == player:
taken_spots += 1
elif board[cell] != 'X' and board[cell] != 'O':
open_spot = some(cell)
if taken_spots == 2 and isSome(open_spot):
return open_spot
return none(int)

proc winning_spot(player: char, board: Board): int =
var spot = try_winning_spot(player, board)
if isSome(spot):
return spot.get
return random_spot(board)

proc winning_or_blocking_spot(player: char, board: Board): int =
var spot = try_winning_spot(player, board)
if isSome(spot):
return spot.get
spot = try_winning_spot(swap_player(player), board)
if isSome(spot):
return spot.get
return random_spot(board)

proc digit_to_char(d: range[0..9]): char = char(48 + d) # 48 = '0'

# This (and minmax_spot) actually always leaves the board in the same state
# as it was before the call. But it does temporarily change the board state
# trade off here is between not declaring and making a copy which is clearer
# but less efficient and the current approach which is more efficient but
# less clear.
proc get_optimal_spot(player: char, board: var Board): Move =
var
best_move = Move(spot: 1, score: Score.LOSE)
empty_spots: seq[int] = @[]

if is_winner(player, board):
best_move.score = Score.WIN
return best_move
if is_winner(swap_player(player), board):
best_move.score = Score.LOSE
return best_move

empty_spots = get_empty_spots(board)
if empty_spots.len == 0:
best_move.score = Score.DRAW
return best_move
if empty_spots.len == board.len:
best_move.spot = sample(empty_spots)
return best_move

for spot in empty_spots:
board[spot] = player
var score = -get_optimal_spot(swap_player(player), board).score
board[spot] = digit_to_char(spot)
if score > best_move.score:
best_move.spot = spot
best_move.score = score
return best_move

proc minmax_spot(player: char, board: var Board): int =
get_optimal_spot(player, board).spot

proc ai_turn(player: char, board: var Board, strength: int) =
echo "AI turn as player", player, " with strength ", strength, "."
show_board(board)
var best_spot = case strength
of 1:
random_spot(board)
of 2:
winning_spot(player, board)
of 3:
winning_or_blocking_spot(player, board)
else:
minmax_spot(player, board)
board[best_spot] = player
sleep 1000 # Sleeps for 1000ms = 1s

proc play_game*(X_strength, O_strength: int) =
var
board: Board = ['1', '2', '3', '4', '5', '6', '7', '8', '9']
player: char = 'X'
while true:
if player == 'X' and X_strength > 0:
ai_turn(player, board, X_strength)
elif player == 'O' and O_strength > 0:
ai_turn(player, board, O_strength)
else:
player_turn(player, board)
if is_winner(player, board):
echo "Player ", player, " wins!"
break
elif is_full(board):
echo "It's a draw!"
break
player = swap_player(player)
show_board(board)
Loading

0 comments on commit 6c8eef4

Please sign in to comment.