diff --git a/config.json b/config.json index 707dae2ce3..13345d628b 100644 --- a/config.json +++ b/config.json @@ -668,6 +668,21 @@ ], "difficulty": 2 }, + { + "slug": "eliuds-eggs", + "name": "Eliud's Eggs", + "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "practices": ["loops"], + "prerequisites": [ + "basics", + "lists", + "list-methods", + "loops", + "strings", + "string-methods" + ], + "difficulty": 2 + }, { "slug": "protein-translation", "name": "Protein Translation", @@ -970,17 +985,14 @@ "difficulty": 3 }, { - "slug": "eliuds-eggs", - "name": "Eliud's Eggs", - "uuid": "356e2d29-7efc-4fa3-bec7-8b61c3e967da", + "slug": "state-of-tic-tac-toe", + "name": "State of Tic-Tac-Toe", + "uuid": "cb40b36f-f7dc-4018-aad5-38976defb352", "practices": ["loops"], "prerequisites": [ - "basics", - "lists", - "list-methods", - "loops", - "strings", - "string-methods" + "conditionals", + "lists", + "loops" ], "difficulty": 3 }, 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 0000000000..1a03ebb6cb --- /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/config.json b/exercises/practice/state-of-tic-tac-toe/.meta/config.json new file mode 100644 index 0000000000..737a603c6e --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "state_of_tic_tac_toe.py" + ], + "test": [ + "state_of_tic_tac_toe_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "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.py b/exercises/practice/state-of-tic-tac-toe/.meta/example.py new file mode 100644 index 0000000000..5f0bf70291 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/example.py @@ -0,0 +1,49 @@ +def gamestate(board): + def check_if_won(player): + # Rows + for row in board: + if all(cell == player for cell in row): + return True + + # Cols + for col in range(3): + if all(board[row][col] == player for row in range(3)): + return True + + # top left to bottom right + if all(board[i][i] == player for i in range(3)): + return True + + # top right to bottom left + if all(board[i][2-i] == player for i in range(3)): + return True + return False + + x_count = sum(row.count("X") for row in board) + o_count = sum(row.count("O") for row in board) + + if o_count > x_count: + raise ValueError("Wrong turn order: O started") + if x_count > o_count + 1: + raise ValueError("Wrong turn order: X went twice") + + x_won = check_if_won("X") + o_won = check_if_won("O") + + if x_won and o_won: + raise ValueError("Impossible board: game should have ended after the game was won") + + if x_won: + if x_count == o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if o_won: + if x_count > o_count: + raise ValueError("Impossible board: game should have ended after the game was won") + return "win" + + if x_count + o_count == 9: + return "draw" + + return "ongoing" diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 new file mode 100644 index 0000000000..826f2703b4 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/.meta/template.j2 @@ -0,0 +1,28 @@ +{% import "generator_macros.j2" as macros with context %} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +{% macro test_case(case) -%} + def test_{{ case["description"]|to_snake }}(self): + board = [ + {% for row in case["input"]["board"] -%} + "{{ row }}", + {% endfor -%} + ] + {%- if case is error_case %} + with self.assertRaises(ValueError) as err: + {{ case["property"]|to_snake }}(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}") + {%- else %} + self.assertEqual({{ case["property"]|to_snake }}(board), "{{ case["expected"] }}") + {%- endif %} +{%- endmacro %} + +class {{ exercise|camel_case }}Test(unittest.TestCase): + {%- for category in cases %} + {%- for case in category["cases"] %} + {{ test_case(case) }} + {%- endfor %} + {%- endfor %} 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 0000000000..8fc25e2118 --- /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.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py new file mode 100644 index 0000000000..b41c78f383 --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe.py @@ -0,0 +1,2 @@ +def gamestate(board): + pass diff --git a/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py new file mode 100644 index 0000000000..173ecf887a --- /dev/null +++ b/exercises/practice/state-of-tic-tac-toe/state_of_tic_tac_toe_test.py @@ -0,0 +1,245 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/state-of-tic-tac-toe/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from state_of_tic_tac_toe import ( + gamestate, +) + + +class StateOfTicTacToeTest(unittest.TestCase): + def test_finished_game_where_x_won_via_left_column_victory(self): + board = [ + "XOO", + "X ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_column_victory(self): + board = [ + "OXO", + " X ", + " X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_right_column_victory(self): + board = [ + "OOX", + " X", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_left_column_victory(self): + board = [ + "OXX", + "OX ", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_column_victory(self): + board = [ + "XOX", + " OX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_right_column_victory(self): + board = [ + "XXO", + " XO", + " O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_top_row_victory(self): + board = [ + "XXX", + "XOO", + "O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_middle_row_victory(self): + board = [ + "O ", + "XXX", + " O ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_bottom_row_victory(self): + board = [ + " OO", + "O X", + "XXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_top_row_victory(self): + board = [ + "OOO", + "XXO", + "XX ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_middle_row_victory(self): + board = [ + "XX ", + "OOO", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_bottom_row_victory(self): + board = [ + "XOX", + " XX", + "OOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_falling_diagonal_victory(self): + board = [ + "XOO", + " X ", + " X", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_rising_diagonal_victory(self): + board = [ + "O X", + "OX ", + "X ", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_falling_diagonal_victory(self): + board = [ + "OXX", + "OOX", + "X O", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_o_won_via_rising_diagonal_victory(self): + board = [ + " O", + " OX", + "OXX", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_a_row_and_a_column_victory(self): + board = [ + "XXX", + "XOO", + "XOO", + ] + self.assertEqual(gamestate(board), "win") + + def test_finished_game_where_x_won_via_two_diagonal_victories(self): + board = [ + "XOX", + "OXO", + "XOX", + ] + self.assertEqual(gamestate(board), "win") + + def test_draw(self): + board = [ + "XOX", + "XXO", + "OXO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_another_draw(self): + board = [ + "XXO", + "OXX", + "XOO", + ] + self.assertEqual(gamestate(board), "draw") + + def test_ongoing_game_one_move_in(self): + board = [ + " ", + "X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_two_moves_in(self): + board = [ + "O ", + " X ", + " ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_ongoing_game_five_moves_in(self): + board = [ + "X ", + " XO", + "OX ", + ] + self.assertEqual(gamestate(board), "ongoing") + + def test_invalid_board_x_went_twice(self): + board = [ + "XX ", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: X went twice") + + def test_invalid_board_o_started(self): + board = [ + "OOX", + " ", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Wrong turn order: O started") + + def test_invalid_board_x_won_and_o_kept_playing(self): + board = [ + "XXX", + "OOO", + " ", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + ) + + def test_invalid_board_players_kept_playing_after_a_win(self): + board = [ + "XXX", + "OOO", + "XOX", + ] + with self.assertRaises(ValueError) as err: + gamestate(board) + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], + "Impossible board: game should have ended after the game was won", + )