From 484558794df142121d6492bb16a40bb761b2c886 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Tue, 9 Apr 2024 16:57:38 -0400 Subject: [PATCH] add state-of-tic-tac-toe exercise --- config.json | 8 + .../.docs/instructions.md | 101 +++++++ .../state-of-tic-tac-toe/.meta/config.json | 19 ++ .../state-of-tic-tac-toe/.meta/example.tcl | 84 ++++++ .../state-of-tic-tac-toe/.meta/tests.toml | 101 +++++++ .../state-of-tic-tac-toe.tcl | 3 + .../state-of-tic-tac-toe.test | 261 ++++++++++++++++++ .../state-of-tic-tac-toe/testHelpers.tcl | 20 ++ notes-on-unimplemented-exercises.md | 8 +- 9 files changed, 601 insertions(+), 4 deletions(-) create mode 100644 exercises/practice/state-of-tic-tac-toe/.docs/instructions.md create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/config.json create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/example.tcl create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/tests.toml create mode 100644 exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.tcl create mode 100644 exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.test create mode 100644 exercises/practice/state-of-tic-tac-toe/testHelpers.tcl diff --git a/config.json b/config.json index 2a5f0d67..8a76ebf0 100644 --- a/config.json +++ b/config.json @@ -1614,6 +1614,14 @@ "practices": [], "prerequisites": [], "difficulty": 3 + }, + { + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "8e616fd7-5368-4c18-bb94-b50e9bd1677b", + "practices": [], + "prerequisites": [], + "difficulty": 5 } ], "foregone": [ diff --git a/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md new file mode 100644 index 00000000..f525d358 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.docs/instructions.md @@ -0,0 +1,101 @@ +# Instructions + +In this exercise, you're going to implement a program that determines the state of a [tic-tac-toe][] game. +(_You may also know the game as "noughts and crosses" or "Xs and Os"._) + +The games is played on a 3×3 grid. +Players take turns to place `X`s and `O`s on the grid. +The game ends when one player has won by placing three of marks in a row, column, or along a diagonal of the grid, or when the entire grid is filled up. + +In this exercise, we will assume that `X` starts. + +It's your job to determine which state a given game is in. + +There are 3 potential game states: + +- The game is **ongoing**. +- The game ended in a **draw**. +- The game ended in a **win**. + +If the given board is invalid, throw an appropriate error. + +If a board meets the following conditions, it is invalid: + +- The given board cannot be reached when turns are taken in the correct order (remember that `X` starts). +- The game was played after it already ended. + +## Examples + +### Ongoing game + +```text + | | + X | | +___|___|___ + | | + | X | O +___|___|___ + | | + O | X | + | | +``` + +### Draw + +```text + | | + X | O | X +___|___|___ + | | + X | X | O +___|___|___ + | | + O | X | O + | | +``` + +### Win + +```text + | | + X | X | X +___|___|___ + | | + | O | O +___|___|___ + | | + | | + | | +``` + +### Invalid + +#### Wrong turn order + +```text + | | + O | O | X +___|___|___ + | | + | | +___|___|___ + | | + | | + | | +``` + +#### Continued playing after win + +```text + | | + X | X | X +___|___|___ + | | + O | O | O +___|___|___ + | | + | | + | | +``` + +[tic-tac-toe]: https://en.wikipedia.org/wiki/Tic-tac-toe diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/config.json b/exercises/practice/state-of-tic-tac-toe/.meta/config.json new file mode 100644 index 00000000..b7d0345b --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "./state-of-tic-tac-toe.tcl" + ], + "test": [ + "./state-of-tic-tac-toe.test" + ], + "example": [ + ".meta/example.tcl" + ] + }, + "blurb": "Determine the game state of a match of Tic-Tac-Toe.", + "source": "Created by Sascha Mann for the Julia track of the Exercism Research Experiment.", + "source_url": "https://github.com/exercism/research_experiment_1/tree/julia-dev/exercises/julia-1-a" +} diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/example.tcl b/exercises/practice/state-of-tic-tac-toe/.meta/example.tcl new file mode 100644 index 00000000..cdb2e6c3 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/example.tcl @@ -0,0 +1,84 @@ +# This solution encodes the X and O positions into an integer. +# The cells of the board are numbered like this: +# 1 2 3 +# 4 5 6 +# 7 8 9 +# +# Given a board like +# X X O +# O X . +# . X O +# The value for X will have bits 1, 2, 5, 8 set == 010010011 == 147 +# The value for O will have bits 3, 4, 9 set == 100001100 == 268 + +namespace eval StateOfTicTacToe { + namespace export gamestate + + variable wins { + 0b001001001 + 0b010010010 + 0b100100100 + 0b000000111 + 0b000111000 + 0b111000000 + 0b100010001 + 0b001010100 + } + + proc gamestate {board} { + lassign [parse $board] X O + + switch [list [won $X] [won $O]] { + {true true} {error "Impossible board: game should have ended after the game was won"} + + {true false} - + {false true} {return "win"} + + default { + set xBits [bitCount $X] + set oBits [bitCount $O] + if {$xBits - $oBits > 1} then {error "Wrong turn order: X went twice"} + if {$xBits - $oBits < 0} then {error "Wrong turn order: O started"} + return [expr {$xBits + $oBits == 9 ? "draw" : "ongoing"}] + } + } + } + + proc parse {board} { + set X 0 + set O 0 + set i 1 + + foreach cell [split [string cat {*}$board] ""] { + if {$cell in {X O}} then {setBit $cell $i} + incr i + } + return [list $X $O] + } + + proc setBit {varname pos} { + upvar 1 $varname var + set var [expr {$var | (1 << ($pos - 1))}] + } + + proc bitCount {n} { + set count 0 + while {$n > 0} { + incr count [expr {$n & 1}] + set n [expr {$n >> 1}] + } + return $count + } + + proc won {player} { + variable wins + foreach combo $wins { + if {($player & $combo) == $combo} { + return true + } + } + return false + } +} + +namespace import StateOfTicTacToe::gamestate diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml new file mode 100644 index 00000000..8fc25e21 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml @@ -0,0 +1,101 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[fe8e9fa9-37af-4d7e-aa24-2f4b8517161a] +description = "Won games -> Finished game where X won via left column victory" + +[96c30df5-ae23-4cf6-bf09-5ef056dddea1] +description = "Won games -> Finished game where X won via middle column victory" + +[0d7a4b0a-2afd-4a75-8389-5fb88ab05eda] +description = "Won games -> Finished game where X won via right column victory" + +[bd1007c0-ec5d-4c60-bb9f-1a4f22177d51] +description = "Won games -> Finished game where O won via left column victory" + +[c032f800-5735-4354-b1b9-46f14d4ee955] +description = "Won games -> Finished game where O won via middle column victory" + +[662c8902-c94a-4c4c-9d9c-e8ca513db2b4] +description = "Won games -> Finished game where O won via right column victory" + +[2d62121f-7e3a-44a0-9032-0d73e3494941] +description = "Won games -> Finished game where X won via top row victory" + +[108a5e82-cc61-409f-aece-d7a18c1beceb] +description = "Won games -> Finished game where X won via middle row victory" +include = false + +[346527db-4db9-4a96-b262-d7023dc022b0] +description = "Won games -> Finished game where X won via middle row victory" +reimplements = "108a5e82-cc61-409f-aece-d7a18c1beceb" + +[a013c583-75f8-4ab2-8d68-57688ff04574] +description = "Won games -> Finished game where X won via bottom row victory" + +[2c08e7d7-7d00-487f-9442-e7398c8f1727] +description = "Won games -> Finished game where O won via top row victory" + +[bb1d6c62-3e3f-4d1a-9766-f8803c8ed70f] +description = "Won games -> Finished game where O won via middle row victory" + +[6ef641e9-12ec-44f5-a21c-660ea93907af] +description = "Won games -> Finished game where O won via bottom row victory" + +[ab145b7b-26a7-426c-ab71-bf418cd07f81] +description = "Won games -> Finished game where X won via falling diagonal victory" + +[7450caab-08f5-4f03-a74b-99b98c4b7a4b] +description = "Won games -> Finished game where X won via rising diagonal victory" + +[c2a652ee-2f93-48aa-a710-a70cd2edce61] +description = "Won games -> Finished game where O won via falling diagonal victory" + +[5b20ceea-494d-4f0c-a986-b99efc163bcf] +description = "Won games -> Finished game where O won via rising diagonal victory" + +[035a49b9-dc35-47d3-9d7c-de197161b9d4] +description = "Won games -> Finished game where X won via a row and a column victory" + +[e5dfdeb0-d2bf-4b5a-b307-e673f69d4a53] +description = "Won games -> Finished game where X won via two diagonal victories" + +[b42ed767-194c-4364-b36e-efbfb3de8788] +description = "Drawn games -> Draw" + +[227a76b2-0fef-4e16-a4bd-8f9d7e4c3b13] +description = "Drawn games -> Another draw" + +[4d93f15c-0c40-43d6-b966-418b040012a9] +description = "Ongoing games -> Ongoing game: one move in" + +[c407ae32-4c44-4989-b124-2890cf531f19] +description = "Ongoing games -> Ongoing game: two moves in" + +[199b7a8d-e2b6-4526-a85e-78b416e7a8a9] +description = "Ongoing games -> Ongoing game: five moves in" + +[1670145b-1e3d-4269-a7eb-53cd327b302e] +description = "Invalid boards -> Invalid board: X went twice" + +[47c048e8-b404-4bcf-9e51-8acbb3253f3b] +description = "Invalid boards -> Invalid board: O started" + +[b1dc8b13-46c4-47db-a96d-aa90eedc4e8d] +description = "Invalid boards -> Invalid board" +include = false + +[6c1920f2-ab5c-4648-a0c9-997414dda5eb] +description = "Invalid boards -> Invalid board: X won and O kept playing" +reimplements = "b1dc8b13-46c4-47db-a96d-aa90eedc4e8d" + +[4801cda2-f5b7-4c36-8317-3cdd167ac22c] +description = "Invalid boards -> Invalid board: players kept playing after a win" diff --git a/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.tcl b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.tcl new file mode 100644 index 00000000..8b075534 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.tcl @@ -0,0 +1,3 @@ +proc gamestate {board} { + throw {NOT_IMPLEMENTED} "Implement this procedure." +} diff --git a/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.test b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.test new file mode 100644 index 00000000..c4423798 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.test @@ -0,0 +1,261 @@ +#!/usr/bin/env tclsh +package require tcltest +namespace import ::tcltest::* +source testHelpers.tcl + +############################################################ +source "state-of-tic-tac-toe.tcl" + +# Won games + +test state-1.1 "Finished game where X won via left column victory" -body { + gamestate { + "XOO" + "X " + "X " + } +} -returnCodes ok -result "win" + +skip state-1.2 +test state-1.2 "Finished game where X won via middle column victory" -body { + gamestate { + "OXO" + " X " + " X " + } +} -returnCodes ok -result "win" + +skip state-1.3 +test state-1.3 "Finished game where X won via right column victory" -body { + gamestate { + "OOX" + " X" + " X" + } +} -returnCodes ok -result "win" + +skip state-1.4 +test state-1.4 "Finished game where O won via left column victory" -body { + gamestate { + "OXX" + "OX " + "O " + } +} -returnCodes ok -result "win" + +skip state-1.5 +test state-1.5 "Finished game where O won via middle column victory" -body { + gamestate { + "XOX" + " OX" + " O " + } +} -returnCodes ok -result "win" + +skip state-1.6 +test state-1.6 "Finished game where O won via right column victory" -body { + gamestate { + "XXO" + " XO" + " O" + } +} -returnCodes ok -result "win" + +skip state-1.7 +test state-1.7 "Finished game where X won via top row victory" -body { + gamestate { + "XXX" + "XOO" + "O " + } +} -returnCodes ok -result "win" + +skip state-1.8 +test state-1.8 "Finished game where X won via middle row victory" -body { + gamestate { + "O " + "XXX" + " O " + } +} -returnCodes ok -result "win" + +skip state-1.9 +test state-1.9 "Finished game where X won via bottom row victory" -body { + gamestate { + " OO" + "O X" + "XXX" + } +} -returnCodes ok -result "win" + +skip state-1.10 +test state-1.10 "Finished game where O won via top row victory" -body { + gamestate { + "OOO" + "XXO" + "XX " + } +} -returnCodes ok -result "win" + +skip state-1.11 +test state-1.11 "Finished game where O won via middle row victory" -body { + gamestate { + "XX " + "OOO" + "X " + } +} -returnCodes ok -result "win" + +skip state-1.12 +test state-1.12 "Finished game where O won via bottom row victory" -body { + gamestate { + "XOX" + " XX" + "OOO" + } +} -returnCodes ok -result "win" + +skip state-1.13 +test state-1.13 "Finished game where X won via falling diagonal victory" -body { + gamestate { + "XOO" + " X " + " X" + } +} -returnCodes ok -result "win" + +skip state-1.14 +test state-1.14 "Finished game where X won via rising diagonal victory" -body { + gamestate { + "O X" + "OX " + "X " + } +} -returnCodes ok -result "win" + +skip state-1.15 +test state-1.15 "Finished game where O won via falling diagonal victory" -body { + gamestate { + "OXX" + "OOX" + "X O" + } +} -returnCodes ok -result "win" + +skip state-1.16 +test state-1.16 "Finished game where O won via rising diagonal victory" -body { + gamestate { + " O" + " OX" + "OXX" + } +} -returnCodes ok -result "win" + +skip state-1.17 +test state-1.17 "Finished game where X won via a row and a column victory" -body { + gamestate { + "XXX" + "XOO" + "XOO" + } +} -returnCodes ok -result "win" + +skip state-1.18 +test state-1.18 "Finished game where X won via two diagonal victories" -body { + gamestate { + "XOX" + "OXO" + "XOX" + } +} -returnCodes ok -result "win" + +# Drawn games + +skip state-2.1 +test state-2.1 "Draw" -body { + gamestate { + "XOX" + "XXO" + "OXO" + } +} -returnCodes ok -result "draw" + +skip state-2.2 +test state-2.2 "Another draw" -body { + gamestate { + "XXO" + "OXX" + "XOO" + } +} -returnCodes ok -result "draw" + + +# Ongoing games + +skip state-3.1 +test state-3.1 "Ongoing game: one move in" -body { + gamestate { + " " + "X " + " " + } +} -returnCodes ok -result "ongoing" + +skip state-3.2 +test state-3.2 "Ongoing game: two moves in" -body { + gamestate { + "O " + " X " + " " + } +} -returnCodes ok -result "ongoing" + +skip state-3.3 +test state-3.3 "Ongoing game: five moves in" -body { + gamestate { + "X " + " XO" + "OX " + } +} -returnCodes ok -result "ongoing" + +# Invalid boards + +skip state-4.1 +test state-4.1 "Invalid board: X went twice" -body { + gamestate { + "XX " + " " + " " + } +} -returnCodes error -result "Wrong turn order: X went twice" + +skip state-4.2 +test state-4.2 "Invalid board: O started" -body { + gamestate { + "OOX" + " " + " " + } +} -returnCodes error -result "Wrong turn order: O started" + +skip state-4.3 +test state-4.3 "Invalid board: X won and O kept playing" -body { + gamestate { + "XXX" + "OOO" + " " + } +} -returnCodes error -result "Impossible board: game should have ended after the game was won" + +skip state-4.4 +test state-4.4 "Invalid board: players kept playing after a win" -body { + gamestate { + "XXX" + "OOO" + "XOX" + } +} -returnCodes error -result "Impossible board: game should have ended after the game was won" + + +cleanupTests diff --git a/exercises/practice/state-of-tic-tac-toe/testHelpers.tcl b/exercises/practice/state-of-tic-tac-toe/testHelpers.tcl new file mode 100644 index 00000000..292829fb --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/testHelpers.tcl @@ -0,0 +1,20 @@ +############################################################# +# Override some tcltest procs with additional functionality + +# Allow an environment variable to override `skip` +proc skip {patternList} { + if { [info exists ::env(RUN_ALL)] + && [string is boolean -strict $::env(RUN_ALL)] + && $::env(RUN_ALL) + } then return else { + uplevel 1 [list ::tcltest::skip $patternList] + } +} + +# Exit non-zero if any tests fail. +# The cleanupTests resets the numTests array, so capture it first. +proc cleanupTests {} { + set failed [expr {$::tcltest::numTests(Failed) > 0}] + uplevel 1 ::tcltest::cleanupTests + if {$failed} then {exit 1} +} diff --git a/notes-on-unimplemented-exercises.md b/notes-on-unimplemented-exercises.md index 5feffc4c..3259a5e0 100644 --- a/notes-on-unimplemented-exercises.md +++ b/notes-on-unimplemented-exercises.md @@ -10,7 +10,8 @@ ## Merely unimplemented -* [killer-sudoku-helper][killer-sudoku-helper] +* [bottle-song][bottle-song] +* [game-of-life][game-of-life] * [ledger][ledger] - a tedious-to-create refactoring exercise * [lens-person][lens-person] @@ -20,7 +21,6 @@ - probably need to use the [`transchan`][transchan] command - [this stackoverflow question][tcl-tee] may provide a starting point * [sgf-parsing][sgf-parsing] -* [state-of-tic-tac-toe][state-of-tic-tac-toe] * [tree-building][tree-building] - a tedious-to-create refactoring exercise @@ -34,6 +34,6 @@ [tcl-tee]: https://stackoverflow.com/q/72352064/7552 [sgf-parsing]: https://github.com/exercism/problem-specifications/tree/master/exercises/sgf-parsing [tree-building]: https://github.com/exercism/problem-specifications/tree/master/exercises/tree-building -[killer-sudoku-helper]: https://github.com/exercism/problem-specifications/tree/master/exercises/killer-sudoku-helper -[state-of-tic-tac-toe]: https://github.com/exercism/problem-specifications/tree/master/exercises/state-of-tic-tac-toe +[bottle-song]: https://github.com/exercism/problem-specifications/tree/master/exercises/bottle-song +[game-of-life]: https://github.com/exercism/problem-specifications/tree/master/exercises/game-of-life [transchan]: https://www.tcl-lang.org/man/tcl8.6/TclCmd/transchan.htm