@@ 113,6 113,14 @@ optional = false
python-versions = "*"
[[package]]
+name = "numpy"
+version = "1.22.0"
+description = "NumPy is the fundamental package for array computing with Python."
+category = "main"
+optional = false
+python-versions = ">=3.8"
+
+[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
@@ 302,8 310,8 @@ python-versions = "*"
[metadata]
lock-version = "1.1"
-python-versions = "^3.8"
-content-hash = "504a69dcd75f1614ae270cac6df80283a10172343545e30bfc1b0069091605ea"
+python-versions = "3.8"
+content-hash = "6d02df6b55ba87f647d238f9368d8b8a8b66076fd3aec616a4ed8eeca10f96c6"
[metadata.files]
astroid = [
@@ 403,6 411,30 @@ neat-python = [
{file = "neat-python-0.92.tar.gz", hash = "sha256:be722a62d053b39fe960228e3e0baffdebe73074133ce40bbd35de650ca53a0f"},
{file = "neat_python-0.92-py3-none-any.whl", hash = "sha256:c69f3748032cc9653b902f0cca05983b79f5288c67aed747aa47279161117059"},
]
+numpy = [
+ {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d22662b4b10112c545c91a0741f2436f8ca979ab3d69d03d19322aa970f9695"},
+ {file = "numpy-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a1f3816ea82eed4178102c56281782690ab5993251fdfd75039aad4d20385f"},
+ {file = "numpy-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5dc65644f75a4c2970f21394ad8bea1a844104f0fe01f278631be1c7eae27226"},
+ {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c16cec1c8cf2728f1d539bd55aaa9d6bb48a7de2f41eb944697293ef65a559"},
+ {file = "numpy-1.22.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97e82c39d9856fe7d4f9b86d8a1e66eff99cf3a8b7ba48202f659703d27c46f"},
+ {file = "numpy-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:e41e8951749c4b5c9a2dc5fdbc1a4eec6ab2a140fdae9b460b0f557eed870f4d"},
+ {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bece0a4a49e60e472a6d1f70ac6cdea00f9ab80ff01132f96bd970cdd8a9e5a9"},
+ {file = "numpy-1.22.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:818b9be7900e8dc23e013a92779135623476f44a0de58b40c32a15368c01d471"},
+ {file = "numpy-1.22.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:47ee7a839f5885bc0c63a74aabb91f6f40d7d7b639253768c4199b37aede7982"},
+ {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a024181d7aef0004d76fb3bce2a4c9f2e67a609a9e2a6ff2571d30e9976aa383"},
+ {file = "numpy-1.22.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f71d57cc8645f14816ae249407d309be250ad8de93ef61d9709b45a0ddf4050c"},
+ {file = "numpy-1.22.0-cp38-cp38-win32.whl", hash = "sha256:283d9de87c0133ef98f93dfc09fad3fb382f2a15580de75c02b5bb36a5a159a5"},
+ {file = "numpy-1.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:2762331de395739c91f1abb88041f94a080cb1143aeec791b3b223976228af3f"},
+ {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:76ba7c40e80f9dc815c5e896330700fd6e20814e69da9c1267d65a4d051080f1"},
+ {file = "numpy-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0cfe07133fd00b27edee5e6385e333e9eeb010607e8a46e1cd673f05f8596595"},
+ {file = "numpy-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6ed0d073a9c54ac40c41a9c2d53fcc3d4d4ed607670b9e7b0de1ba13b4cbfe6f"},
+ {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41388e32e40b41dd56eb37fcaa7488b2b47b0adf77c66154d6b89622c110dfe9"},
+ {file = "numpy-1.22.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b55b953a1bdb465f4dc181758570d321db4ac23005f90ffd2b434cc6609a63dd"},
+ {file = "numpy-1.22.0-cp39-cp39-win32.whl", hash = "sha256:5a311ee4d983c487a0ab546708edbdd759393a3dc9cd30305170149fedd23c88"},
+ {file = "numpy-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:a97a954a8c2f046d3817c2bce16e3c7e9a9c2afffaf0400f5c16df5172a67c9c"},
+ {file = "numpy-1.22.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb02929b0d6bfab4c48a79bd805bd7419114606947ec8284476167415171f55b"},
+ {file = "numpy-1.22.0.zip", hash = "sha256:a955e4128ac36797aaffd49ab44ec74a71c11d6938df83b1285492d277db5397"},
+]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
@@ 1,7 1,115 @@
"""Game logic module."""
-from .window import open_window
+from random import sample
+from typing import List, Optional, Tuple
+import numpy as np
-def start_game():
- """Start game."""
- open_window()
+#pylint: disable=invalid-name
+
+class State:
+ """Represents game state at any point in time.
+
+ Properties:
+ size: size of the board,
+ near[x, y]: how many bombs are near `(x, y)`
+ bomb[x, y]: 1 if `(x, y)` is a bomb
+ revealed[x, y]: 1 if `(x, y)` has been revealed
+ flagged[x, y]: 1 if `(x, y)` has been flagged
+ won: `True` if user has won, `None` if the game has not finished
+ """
+
+ def __init__(
+ self,
+ size: int,
+ bombs: float = 0.0,
+ bomb_positions: Optional[List[Tuple[int, int]]] = None,
+ ):
+ """Create a new game state.
+
+ Args:
+ size: board size
+ bombs: percentage of bombs to place
+ bomb_positions: exact bomb positions
+
+ If `bomb_positions` are provided the `bombs` are ignored.
+ """
+ self.size = size
+ self.bomb = np.zeros((self.size, self.size), dtype=int)
+ self.revealed = np.copy(self.bomb)
+ self.flagged = np.copy(self.bomb)
+ self.near = np.copy(self.bomb)
+ self.won: Optional[bool] = None
+
+ # If the positions were not explicitly provided randomise them.
+ if bomb_positions is None:
+ bomb_positions = list(
+ zip(
+ sample(range(self.size), k=int(self.size**2 * bombs)),
+ sample(range(self.size), k=int(self.size**2 * bombs)),
+ ))
+
+ # Create the board based on the positions
+ for x, y in bomb_positions:
+ self.bomb[x, y] = 1
+ for n_x, n_y in self.neighbors(x, y):
+ self.near[n_x, n_y] += 1
+
+
+ @property
+ def not_bomb(self):
+ """The inverse of `self.bombs`."""
+ return (self.bomb + 1) % 2
+
+ @property
+ def not_revealed(self):
+ """The inverse of `self.revealed`."""
+ return (self.revealed + 1) % 2
+
+
+ def click(self, x: int, y: int):
+ """Simulate a click on the `(x, y)` position.
+
+ Sets `self.won` to `True` if the user has won and shows the
+ bombs, if `(x, y)` is a bomb sets `self.won` to `False`.
+
+ If the game has not ended performs a recursive reveal.
+ """
+ if self.bomb[x, y] == 1:
+ # Show all the bombs
+ self.revealed[self.bomb == 1] = 1
+ self.won = False
+ return
+
+ if not self.revealed[x, y]:
+ self.reveal(x, y)
+
+ # Check if the player has won
+ if np.array_equal(self.revealed, self.not_bomb):
+ self.won = True
+
+ def reveal(self, x: int, y: int):
+ """Reveals `(x, y)`.
+
+ If `(x, y)` is not near a bomb the reveal cascades recursively,
+ revealing the neighbors too.
+ """
+ self.revealed[x, y] = 1
+ # If there are no bombs nearby then cascade
+ if self.near[x, y] == 0:
+ for n_x, n_y in self.neighbors(x, y):
+ if self.bomb[n_x, n_y] == 0 and self.revealed[n_x, n_y] == 0:
+ self.reveal(n_x, n_y)
+
+ def neighbors(self, x: int, y: int) -> List[Tuple[int, int]]:
+ """Return list of all valid neighbors of `(x, y)`."""
+ neighbors = []
+ for new_x in [x - 1, x, x + 1]:
+ for new_y in [y - 1, y, y + 1]:
+ if 0 <= new_x < self.size and 0 <= new_y < self.size:
+ if not (new_x == x and new_y == y):
+ neighbors.append((new_x, new_y))
+ return neighbors
+
+ def flag(self, x: int, y: int):
+ """Mark `(x, y)` as flagged."""
+ self.flagged[x, y] = 1
@@ 1,9 1,143 @@
"""Module for testing core game logic."""
+import numpy as np
+from sweep_ai import logic
-def test_add():
- assert (1 + 1) == 2
+def test_bombs():
+ state = logic.State(4, bomb_positions=[(0, 0), (3, 2)])
+ assert np.array_equal(
+ state.bomb,
+ np.array([
+ [1, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 1, 0],
+ ]))
+ assert np.array_equal(
+ state.not_bomb,
+ np.array([
+ [0, 1, 1, 1],
+ [1, 1, 1, 1],
+ [1, 1, 1, 1],
+ [1, 1, 0, 1],
+ ]))
-def test_subtract():
- assert (1 - 1) == 0
+
+def test_neighbors():
+ state = logic.State(4)
+ assert state.neighbors(0, 0) == [(0, 1), (1, 0), (1, 1)]
+ assert state.neighbors(3, 3) == [(2, 2), (2, 3), (3, 2)]
+ assert state.neighbors(1, 2) == [
+ (0, 1),
+ (0, 2),
+ (0, 3),
+ (1, 1),
+ (1, 3),
+ (2, 1),
+ (2, 2),
+ (2, 3),
+ ]
+
+
+def test_near():
+ state = logic.State(4, bomb_positions=[(0, 0)])
+ assert np.array_equal(
+ state.near,
+ np.array([
+ [0, 1, 0, 0],
+ [1, 1, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ ]))
+
+ state = logic.State(4, bomb_positions=[(1, 1), (3, 0)])
+ assert np.array_equal(
+ state.near,
+ np.array([
+ [1, 1, 1, 0],
+ [1, 0, 1, 0],
+ [2, 2, 1, 0],
+ [0, 1, 0, 0],
+ ]))
+
+
+def test_reveal():
+ state = logic.State(4, bomb_positions=[(0, 0)])
+ state.reveal(3, 3)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [0, 1, 1, 1],
+ [1, 1, 1, 1],
+ [1, 1, 1, 1],
+ [1, 1, 1, 1],
+ ]))
+ assert np.array_equal(
+ state.not_revealed,
+ np.array([
+ [1, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ ]))
+
+ state = logic.State(4, bomb_positions=[(0, 0), (1, 1)])
+ state.reveal(3, 3)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [0, 0, 1, 1],
+ [0, 0, 1, 1],
+ [1, 1, 1, 1],
+ [1, 1, 1, 1],
+ ]))
+
+ state = logic.State(4, bomb_positions=[(0, 0), (1, 1), (3, 3)])
+ state.reveal(3, 0)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [1, 1, 1, 0],
+ [1, 1, 1, 0],
+ ]))
+
+
+def test_click():
+ state = logic.State(4, bomb_positions=[(0, 0), (1, 1), (3, 3)])
+ state.click(3, 0)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [1, 1, 1, 0],
+ [1, 1, 1, 0],
+ ]))
+ assert state.won is None
+
+ # The same click again - should be a no-op
+ state.click(3, 0)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [0, 0, 0, 0],
+ [0, 0, 0, 0],
+ [1, 1, 1, 0],
+ [1, 1, 1, 0],
+ ]))
+ assert state.won is None
+
+ # Click on a bomb = defeat
+ state.click(1, 1)
+ assert np.array_equal(
+ state.revealed,
+ np.array([
+ [1, 0, 0, 0],
+ [0, 1, 0, 0],
+ [1, 1, 1, 0],
+ [1, 1, 1, 1],
+ ]))
+ assert not state.won