From 59263a82a2e941526dc2763383b15b905563f59d Mon Sep 17 00:00:00 2001 From: keiravillekode Date: Mon, 7 Oct 2024 07:01:59 +1100 Subject: [PATCH] Add state-of-tic-tac-toe exercise (#523) --- config.json | 8 + .../practice/state-of-tic-tac-toe/.busted | 5 + .../.docs/instructions.md | 101 +++++++ .../state-of-tic-tac-toe/.meta/config.json | 19 ++ .../state-of-tic-tac-toe/.meta/example.lua | 49 ++++ .../.meta/spec_generator.lua | 37 +++ .../state-of-tic-tac-toe/.meta/tests.toml | 101 +++++++ .../state-of-tic-tac-toe.lua | 5 + .../state-of-tic-tac-toe_spec.lua | 271 ++++++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 exercises/practice/state-of-tic-tac-toe/.busted 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.lua create mode 100644 exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua 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.lua create mode 100644 exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe_spec.lua diff --git a/config.json b/config.json index 809c1e16..debfea14 100644 --- a/config.json +++ b/config.json @@ -1156,6 +1156,14 @@ "text_formatting" ] }, + { + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "e80f466a-315a-4b2b-9277-644fb7dd76ec", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "tournament", "name": "Tournament", diff --git a/exercises/practice/state-of-tic-tac-toe/.busted b/exercises/practice/state-of-tic-tac-toe/.busted new file mode 100644 index 00000000..86b84e7c --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} 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..ae7be379 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "state-of-tic-tac-toe.lua" + ], + "test": [ + "state-of-tic-tac-toe_spec.lua" + ], + "example": [ + ".meta/example.lua" + ] + }, + "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.lua b/exercises/practice/state-of-tic-tac-toe/.meta/example.lua new file mode 100644 index 00000000..f6e98dac --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/example.lua @@ -0,0 +1,49 @@ +local function gamestate(board) + local lines = { 0x007, 0x070, 0x700, 0x111, 0x222, 0x444, 0x124, 0x421 } + + local bitset_x = 0 + local bitset_o = 0 + local count_x = 0 + local count_o = 0 + + for row = 1, 3 do + for column = 1, 3 do + local mark = board[row]:sub(column, column) + if mark == 'X' then + bitset_x = bitset_x | (1 << (row * 4 + column - 5)) + count_x = count_x + 1 + elseif mark == 'O' then + bitset_o = bitset_o | (1 << (row * 4 + column - 5)) + count_o = count_o + 1 + end + end + end + + assert(count_x <= count_o + 1, 'Wrong turn order: X went twice') + assert(count_o <= count_x, 'Wrong turn order: O started') + + local win_x = {} + local win_o = {} + + for _, line in ipairs(lines) do + if (bitset_x & line) == line then + table.insert(win_x, line) + elseif (bitset_o & line) == line then + table.insert(win_o, line) + end + end + + assert(#win_x == 0 or #win_o == 0, 'Impossible board: game should have ended after the game was won') + + if #win_x > 0 or #win_o > 0 then + return 'win' + end + + if count_x + count_o == 9 then + return 'draw' + end + + return 'ongoing' +end + +return { gamestate = gamestate } diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua b/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua new file mode 100644 index 00000000..0a4e0720 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua @@ -0,0 +1,37 @@ +local map = function(t, f) + local mapped = {} + for i, v in ipairs(t) do + mapped[i] = f(v) + end + return mapped +end + +local function render_board(board) + return table.concat(map(board, function(row) + return '\'' .. row .. '\', --' + end), '\n') +end + +return { + module_name = 'state_of_tic_tac_toe', + + generate_test = function(case) + if type(case.expected) == 'string' then + local template = [[ + local board = { + %s + } + assert.are.same('%s', state_of_tic_tac_toe.gamestate(board))]] + return template:format(render_board(case.input.board), case.expected) + else + local template = [[ + local board = { + %s + } + assert.has.error(function() + state_of_tic_tac_toe.gamestate(board) + end)]] + return template:format(render_board(case.input.board)) + end + end +} 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.lua b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.lua new file mode 100644 index 00000000..75ee510e --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe.lua @@ -0,0 +1,5 @@ +local function gamestate(board) + +end + +return { gamestate = gamestate } diff --git a/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe_spec.lua b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe_spec.lua new file mode 100644 index 00000000..0784cf11 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state-of-tic-tac-toe_spec.lua @@ -0,0 +1,271 @@ +local state_of_tic_tac_toe = require('state-of-tic-tac-toe') + +describe('state-of-tic-tac-toe', function() + describe('won games', function() + it('finished game where x won via left column victory', function() + local board = { + 'XOO', -- + 'X ', -- + 'X ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via middle column victory', function() + local board = { + 'OXO', -- + ' X ', -- + ' X ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via right column victory', function() + local board = { + 'OOX', -- + ' X', -- + ' X' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via left column victory', function() + local board = { + 'OXX', -- + 'OX ', -- + 'O ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via middle column victory', function() + local board = { + 'XOX', -- + ' OX', -- + ' O ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via right column victory', function() + local board = { + 'XXO', -- + ' XO', -- + ' O' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via top row victory', function() + local board = { + 'XXX', -- + 'XOO', -- + 'O ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via middle row victory', function() + local board = { + 'O O', -- + 'XXX', -- + ' O ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via middle row victory', function() + local board = { + 'O ', -- + 'XXX', -- + ' O ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via bottom row victory', function() + local board = { + ' OO', -- + 'O X', -- + 'XXX' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via top row victory', function() + local board = { + 'OOO', -- + 'XXO', -- + 'XX ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via middle row victory', function() + local board = { + 'XX ', -- + 'OOO', -- + 'X ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via bottom row victory', function() + local board = { + 'XOX', -- + ' XX', -- + 'OOO' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via falling diagonal victory', function() + local board = { + 'XOO', -- + ' X ', -- + ' X' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via rising diagonal victory', function() + local board = { + 'O X', -- + 'OX ', -- + 'X ' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via falling diagonal victory', function() + local board = { + 'OXX', -- + 'OOX', -- + 'X O' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where o won via rising diagonal victory', function() + local board = { + ' O', -- + ' OX', -- + 'OXX' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via a row and a column victory', function() + local board = { + 'XXX', -- + 'XOO', -- + 'XOO' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + + it('finished game where x won via two diagonal victories', function() + local board = { + 'XOX', -- + 'OXO', -- + 'XOX' -- + } + assert.are.same('win', state_of_tic_tac_toe.gamestate(board)) + end) + end) + + describe('drawn games', function() + it('draw', function() + local board = { + 'XOX', -- + 'XXO', -- + 'OXO' -- + } + assert.are.same('draw', state_of_tic_tac_toe.gamestate(board)) + end) + + it('another draw', function() + local board = { + 'XXO', -- + 'OXX', -- + 'XOO' -- + } + assert.are.same('draw', state_of_tic_tac_toe.gamestate(board)) + end) + end) + + describe('ongoing games', function() + it('ongoing game: one move in', function() + local board = { + ' ', -- + 'X ', -- + ' ' -- + } + assert.are.same('ongoing', state_of_tic_tac_toe.gamestate(board)) + end) + + it('ongoing game: two moves in', function() + local board = { + 'O ', -- + ' X ', -- + ' ' -- + } + assert.are.same('ongoing', state_of_tic_tac_toe.gamestate(board)) + end) + + it('ongoing game: five moves in', function() + local board = { + 'X ', -- + ' XO', -- + 'OX ' -- + } + assert.are.same('ongoing', state_of_tic_tac_toe.gamestate(board)) + end) + end) + + describe('invalid boards', function() + it('invalid board: x went twice', function() + local board = { + 'XX ', -- + ' ', -- + ' ' -- + } + assert.has.error(function() + state_of_tic_tac_toe.gamestate(board) + end) + end) + + it('invalid board: o started', function() + local board = { + 'OOX', -- + ' ', -- + ' ' -- + } + assert.has.error(function() + state_of_tic_tac_toe.gamestate(board) + end) + end) + + it('invalid board: x won and o kept playing', function() + local board = { + 'XXX', -- + 'OOO', -- + ' ' -- + } + assert.has.error(function() + state_of_tic_tac_toe.gamestate(board) + end) + end) + + it('invalid board: players kept playing after a win', function() + local board = { + 'XXX', -- + 'OOO', -- + 'XOX' -- + } + assert.has.error(function() + state_of_tic_tac_toe.gamestate(board) + end) + end) + end) +end)