From 3d4e3b98170cb309e0aadc8ab1701c371685452b Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Mon, 21 Oct 2024 16:25:44 +0200 Subject: [PATCH] Add `state-of-tic-tac-toe` exercise --- config.json | 15 + exercises/Exercises.sln | 7 + .../.config/dotnet-tools.json | 12 + .../.docs/instructions.md | 101 ++++++ .../state-of-tic-tac-toe/.meta/Example.fs | 45 +++ .../state-of-tic-tac-toe/.meta/config.json | 19 + .../state-of-tic-tac-toe/.meta/tests.toml | 101 ++++++ .../state-of-tic-tac-toe/StateOfTicTacToe.fs | 7 + .../StateOfTicTacToe.fsproj | 22 ++ .../StateOfTicTacToeTests.fs | 334 ++++++++++++++++++ generators/Generators.fs | 3 + 11 files changed, 666 insertions(+) create mode 100644 exercises/practice/state-of-tic-tac-toe/.config/dotnet-tools.json create mode 100644 exercises/practice/state-of-tic-tac-toe/.docs/instructions.md create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/Example.fs create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/config.json create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/tests.toml create mode 100644 exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fs create mode 100644 exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fsproj create mode 100644 exercises/practice/state-of-tic-tac-toe/StateOfTicTacToeTests.fs diff --git a/config.json b/config.json index d2a270f9e..dcffe3ed5 100644 --- a/config.json +++ b/config.json @@ -2218,6 +2218,21 @@ "recursion" ], "difficulty": 5 + }, + { + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "4cb569ac-609b-49fd-a8cb-3da0be698c4c", + "practices": [ + "multi-dimensional-arrays", + "result" + ], + "prerequisites": [ + "multi-dimensional-arrays", + "chars", + "result" + ], + "difficulty": 5 } ], "foregone": [ diff --git a/exercises/Exercises.sln b/exercises/Exercises.sln index b676be98e..40399b4bf 100644 --- a/exercises/Exercises.sln +++ b/exercises/Exercises.sln @@ -307,6 +307,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "ResistorColorTrio", "practi EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "KillerSudokuHelper", "practice\killer-sudoku-helper\KillerSudokuHelper.fsproj", "{FCE9E627-CFF9-4EF3-84BE-D42B354825AA}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "StateOfTicTacToe", "practice\state-of-tic-tac-toe\StateOfTicTacToe.fsproj", "{A12FEF19-5EE8-430E-BD66-2D93ADFC1944}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -904,6 +906,10 @@ Global {FCE9E627-CFF9-4EF3-84BE-D42B354825AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {FCE9E627-CFF9-4EF3-84BE-D42B354825AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {FCE9E627-CFF9-4EF3-84BE-D42B354825AA}.Release|Any CPU.Build.0 = Release|Any CPU + {A12FEF19-5EE8-430E-BD66-2D93ADFC1944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A12FEF19-5EE8-430E-BD66-2D93ADFC1944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A12FEF19-5EE8-430E-BD66-2D93ADFC1944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A12FEF19-5EE8-430E-BD66-2D93ADFC1944}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {B404AA3C-A226-409A-A035-6C1DC66940DD} = {B7E719DB-FB8D-43B4-B529-55FCF6E3DC3F} @@ -1056,5 +1062,6 @@ Global {32F8738C-2782-4881-95C0-C621DC0D7ED9} = {391BEEC4-91A8-43F3-AE94-D5CB9A8FA611} {1850FAE9-5ACB-41D0-91BB-AD17A1021248} = {391BEEC4-91A8-43F3-AE94-D5CB9A8FA611} {FCE9E627-CFF9-4EF3-84BE-D42B354825AA} = {391BEEC4-91A8-43F3-AE94-D5CB9A8FA611} + {A12FEF19-5EE8-430E-BD66-2D93ADFC1944} = {391BEEC4-91A8-43F3-AE94-D5CB9A8FA611} EndGlobalSection EndGlobal diff --git a/exercises/practice/state-of-tic-tac-toe/.config/dotnet-tools.json b/exercises/practice/state-of-tic-tac-toe/.config/dotnet-tools.json new file mode 100644 index 000000000..0f7926bad --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "fantomas-tool": { + "version": "4.7.9", + "commands": [ + "fantomas" + ] + } + } +} \ No newline at end of file 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 000000000..1a03ebb6c --- /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 game 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/Example.fs b/exercises/practice/state-of-tic-tac-toe/.meta/Example.fs new file mode 100644 index 000000000..34c9bc78f --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/Example.fs @@ -0,0 +1,45 @@ +module StateOfTicTacToe + +type EndGameState = + | Win + | Draw + | Ongoing + +type GameError = + | ConsecutiveMovesBySamePlayer + | WrongPlayerStarted + | MoveMadeAfterGameWasDone + +type Cell = char +type Board = Cell [,] + +let won (player: Cell) (board: Board) = + let winning = [| player; player; player |] + + Array.init 3 (fun i -> board[i, i]) = winning || + Array.init 3 (fun i -> board[i, 2 - i]) = winning + || Array.init 3 (fun i -> board[i, *]) |> Array.contains winning + || Array.init 3 (fun i -> board[*, i]) |> Array.contains winning + +let gameState (board: Board) = + let numCells cell = + board + |> Seq.cast + |> Seq.filter ((=) cell) + |> Seq.length + + let numNaughts = numCells 'O' + let numCrosses = numCells 'X' + + if abs (numCrosses - numNaughts) > 1 then + Error ConsecutiveMovesBySamePlayer + elif numNaughts > numCrosses then + Error WrongPlayerStarted + elif won 'X' board && won 'O' board then + Error MoveMadeAfterGameWasDone + elif won 'X' board || won 'O' board then + Ok Win + elif numNaughts + numCrosses = 9 then + Ok Draw + else + Ok Ongoing 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 000000000..f7c609687 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "erikschierboom" + ], + "files": { + "solution": [ + "StateOfTicTacToe.fs" + ], + "test": [ + "StateOfTicTacToeTests.fs" + ], + "example": [ + ".meta/Example.fs" + ] + }, + "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/tests.toml b/exercises/practice/state-of-tic-tac-toe/.meta/tests.toml new file mode 100644 index 000000000..8fc25e211 --- /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/StateOfTicTacToe.fs b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fs new file mode 100644 index 000000000..da839d6dc --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fs @@ -0,0 +1,7 @@ +module StateOfTicTacToe + +// TODO: define the 'EndGameState' type +// TODO: define the 'GameError' type + +let gameState board = + failwith "Please implement the 'gameState' function" diff --git a/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fsproj b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fsproj new file mode 100644 index 000000000..a1b3fe460 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToe.fsproj @@ -0,0 +1,22 @@ + + + net8.0 + false + false + true + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + \ No newline at end of file diff --git a/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToeTests.fs b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToeTests.fs new file mode 100644 index 000000000..5e0e9a4dd --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/StateOfTicTacToeTests.fs @@ -0,0 +1,334 @@ +module StateOfTicTacToeTests + +open FsUnit.Xunit +open Xunit + +open StateOfTicTacToe + +[] +let ``Finished game where X won via left column victory`` () = + let board = + array2D [ + [ 'X'; 'O'; 'O' ] + [ 'X'; ' '; ' ' ] + [ 'X'; ' '; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via middle column victory`` () = + let board = + array2D [ + [ 'O'; 'X'; 'O' ] + [ ' '; 'X'; ' ' ] + [ ' '; 'X'; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via right column victory`` () = + let board = + array2D [ + [ 'O'; 'O'; 'X' ] + [ ' '; ' '; 'X' ] + [ ' '; ' '; 'X' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via left column victory`` () = + let board = + array2D [ + [ 'O'; 'X'; 'X' ] + [ 'O'; 'X'; ' ' ] + [ 'O'; ' '; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via middle column victory`` () = + let board = + array2D [ + [ 'X'; 'O'; 'X' ] + [ ' '; 'O'; 'X' ] + [ ' '; 'O'; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via right column victory`` () = + let board = + array2D [ + [ 'X'; 'X'; 'O' ] + [ ' '; 'X'; 'O' ] + [ ' '; ' '; 'O' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via top row victory`` () = + let board = + array2D [ + [ 'X'; 'X'; 'X' ] + [ 'X'; 'O'; 'O' ] + [ 'O'; ' '; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via middle row victory`` () = + let board = + array2D [ + [ 'O'; ' '; ' ' ] + [ 'X'; 'X'; 'X' ] + [ ' '; 'O'; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via bottom row victory`` () = + let board = + array2D [ + [ ' '; 'O'; 'O' ] + [ 'O'; ' '; 'X' ] + [ 'X'; 'X'; 'X' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via top row victory`` () = + let board = + array2D [ + [ 'O'; 'O'; 'O' ] + [ 'X'; 'X'; 'O' ] + [ 'X'; 'X'; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via middle row victory`` () = + let board = + array2D [ + [ 'X'; 'X'; ' ' ] + [ 'O'; 'O'; 'O' ] + [ 'X'; ' '; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via bottom row victory`` () = + let board = + array2D [ + [ 'X'; 'O'; 'X' ] + [ ' '; 'X'; 'X' ] + [ 'O'; 'O'; 'O' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via falling diagonal victory`` () = + let board = + array2D [ + [ 'X'; 'O'; 'O' ] + [ ' '; 'X'; ' ' ] + [ ' '; ' '; 'X' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via rising diagonal victory`` () = + let board = + array2D [ + [ 'O'; ' '; 'X' ] + [ 'O'; 'X'; ' ' ] + [ 'X'; ' '; ' ' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via falling diagonal victory`` () = + let board = + array2D [ + [ 'O'; 'X'; 'X' ] + [ 'O'; 'O'; 'X' ] + [ 'X'; ' '; 'O' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where O won via rising diagonal victory`` () = + let board = + array2D [ + [ ' '; ' '; 'O' ] + [ ' '; 'O'; 'X' ] + [ 'O'; 'X'; 'X' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via a row and a column victory`` () = + let board = + array2D [ + [ 'X'; 'X'; 'X' ] + [ 'X'; 'O'; 'O' ] + [ 'X'; 'O'; 'O' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``Finished game where X won via two diagonal victories`` () = + let board = + array2D [ + [ 'X'; 'O'; 'X' ] + [ 'O'; 'X'; 'O' ] + [ 'X'; 'O'; 'X' ] + ] + + let expected: Result = Ok Win + gameState board |> should equal expected + +[] +let ``A draw`` () = + let board = + array2D [ + [ 'X'; 'O'; 'X' ] + [ 'X'; 'X'; 'O' ] + [ 'O'; 'X'; 'O' ] + ] + + let expected: Result = Ok Draw + gameState board |> should equal expected + +[] +let ``Another draw`` () = + let board = + array2D [ + [ 'X'; 'X'; 'O' ] + [ 'O'; 'X'; 'X' ] + [ 'X'; 'O'; 'O' ] + ] + + let expected: Result = Ok Draw + gameState board |> should equal expected + +[] +let ``Ongoing game: one move in`` () = + let board = + array2D [ + [ ' '; ' '; ' ' ] + [ 'X'; ' '; ' ' ] + [ ' '; ' '; ' ' ] + ] + + let expected: Result = Ok Ongoing + gameState board |> should equal expected + +[] +let ``Ongoing game: two moves in`` () = + let board = + array2D [ + [ 'O'; ' '; ' ' ] + [ ' '; 'X'; ' ' ] + [ ' '; ' '; ' ' ] + ] + + let expected: Result = Ok Ongoing + gameState board |> should equal expected + +[] +let ``Ongoing game: five moves in`` () = + let board = + array2D [ + [ 'X'; ' '; ' ' ] + [ ' '; 'X'; 'O' ] + [ 'O'; 'X'; ' ' ] + ] + + let expected: Result = Ok Ongoing + gameState board |> should equal expected + +[] +let ``Invalid board: X went twice`` () = + let board = + array2D [ + [ 'X'; 'X'; ' ' ] + [ ' '; ' '; ' ' ] + [ ' '; ' '; ' ' ] + ] + + let expected: Result = Error ConsecutiveMovesBySamePlayer + gameState board + |> should equal expected + +[] +let ``Invalid board: O started`` () = + let board = + array2D [ + [ 'O'; 'O'; 'X' ] + [ ' '; ' '; ' ' ] + [ ' '; ' '; ' ' ] + ] + + let expected: Result = Error WrongPlayerStarted + gameState board + |> should equal expected + +[] +let ``Invalid board: X won and O kept playing`` () = + let board = + array2D [ + [ 'X'; 'X'; 'X' ] + [ 'O'; 'O'; 'O' ] + [ ' '; ' '; ' ' ] + ] + + let expected: Result = Error MoveMadeAfterGameWasDone + gameState board + |> should equal expected + +[] +let ``Invalid board: players kept playing after a win`` () = + let board = + array2D [ + [ 'X'; 'X'; 'X' ] + [ 'O'; 'O'; 'O' ] + [ 'X'; 'O'; 'X' ] + ] + + let expected: Result = Error MoveMadeAfterGameWasDone + gameState board + |> should equal expected diff --git a/generators/Generators.fs b/generators/Generators.fs index 23fe473dd..8b010f89c 100644 --- a/generators/Generators.fs +++ b/generators/Generators.fs @@ -2053,3 +2053,6 @@ type ResistorColorTrio() = type KillerSudokuHelper() = inherit ExerciseGenerator() + +type StateOfTicTacToe() = + inherit ExerciseGenerator()