Queer European MD passionate about IT
Browse Source

Final project: chessboard puzzle game

Davte 1 year ago
parent
commit
17075632d0
4 changed files with 241 additions and 2 deletions
  1. 43 2
      README.md
  2. 126 0
      project.py
  3. 3 0
      requirements.txt
  4. 69 0
      test_project.py

+ 43 - 2
README.md

@@ -1,3 +1,44 @@
-# cs50p
+# Chessboard puzzle game
+#### Video Demo:  <URL HERE>
+#### Background information
+This script implements a puzzle game described
+[here](http://datagenetics.com/blog/december12014/index.html).
+I heard about it thanks to 3blue1brown and Stand-up Maths who played
+it and explained it [here](https://youtu.be/as7Gkm7Y7h4).
 
-CS50P: CS50's Introduction to Programming with Python
+#### Overview of the puzzle:
+- Two players are faced with a challenge by a judge.
+- The judge will show player 1 (P1) a square chessboard covered in coins.
+    Each coin will be put heads or tails by the judge at their will.
+- The judge will show player 1 a tile of the chessboard where a key will
+    be hidden. Player 1 must then flip one and only one coin and then leave
+    the room without communicating with Player 2.
+- Player 2 is then shown the chessboard (as modified by the single flip made
+    by Player 1) and must guess the correct tile where the key is hidden.
+- Player 1 and 2 can agree upon a strategy before starting, but the judge
+    will know what they said and can act accordingly when choosing the coin
+    disposition on the chessboard.
+- No tricks: the players can not communicate after Player 1 is told where the
+    key is; each coin may be only H or T, its orientation is meaningless, and
+    it cannot be put in other states (e.g. on its side).
+
+#### Description
+The script generates three pdf files: Problem, Key and Solution.
+- `Key.pdf` shows the initial 8x8 grid, filled with random heads (H) or tails
+    (T), with an orange coin where the invisible key is hidden.
+    This is how the grid looks like when the judge shows it to Player 1
+    asking them to flip one and only one coin.
+- `Problem.pdf` shows the same grid with just one coin flipped by Player 1.
+    This is how the grid looks like when the judge shows it to Player 2 after
+    Player 1 has flipped one coin.
+- `Solution.pdf` shows the same grid, with an orange coin and a purple coin.
+    The orange one is where the key is hidden, the purple-background coin
+    is the one to flip to communicate where the key is.
+
+##### How to play
+- Show player 1 the `Key.pdf`: a grid with one key hidden.
+- Ask player 1 to flip one and only one coin. They should choose the
+    purple-background coin in `Solution.pdf`, thus producing the same grid
+    shown in `Problem.pdf`.
+- Show Player 2 `Problem.pdf` and ask them to identify where the key is.
+    They should point to the orange coin in `Key.pdf` and `Solution.pdf`.

+ 126 - 0
project.py

@@ -0,0 +1,126 @@
+"""Final project for CS50P - Davte. See README.md for more information."""
+import math
+import random
+from typing import Tuple
+
+import fpdf
+import fpdf.table
+from fpdf.fonts import FontFace
+from fpdf.enums import TextEmphasis
+
+
+BOLD = TextEmphasis.coerce('B')
+
+
+def draw_table(pdf: fpdf.FPDF, state: str, key_position: int = -1, coin_to_flip: int = -1,
+               highlight_parity_bits: bool = False) -> fpdf.table.Table:
+    square_side = int(len(state)**0.5)
+    with pdf.table(text_align='CENTER',
+                   first_row_as_headings=False) as table:
+        for i, v in enumerate(state):
+            if i % square_side == 0:
+                row = table.row()
+            cell_value = {'0': 'H', '1': 'T'}[v]
+            cell_style = FontFace()
+            if i == coin_to_flip:
+                cell_style.fill_color = (153, 0, 153)
+                cell_style.emphasis = BOLD
+            if i == key_position:
+                cell_style.color = (255, 128, 0)
+                cell_style.emphasis = BOLD
+            elif highlight_parity_bits and i in (1, 2, 4, 8, 16, 32):
+                cell_style.color = (119, 136, 153)
+                cell_style.emphasis = BOLD
+            row.cell(cell_value, style=cell_style)
+    return table
+
+
+def get_parity(n: str) -> int:
+    num_bits = int(math.log2(len(n)))
+    parity = 0
+    for i in range(num_bits):
+        block_parity = 0
+        for j, val in enumerate(n):
+            if j & (2**i) == 2**i:
+                block_parity = block_parity ^ int(val)
+        parity += block_parity * (2**i)
+    return parity
+
+
+def get_coin_to_flip(initial_state: str, key_position: int) -> int:
+    current_value = get_parity(initial_state)
+    return current_value ^ key_position
+
+
+def store_pdf(file_name, state, key_position: int = -1, coin_to_flip: int = -1):
+    pdf = fpdf.FPDF(orientation="P", unit="mm", format="A4")
+    pdf.set_auto_page_break(False)
+    pdf.add_page()
+    pdf.set_font("Times", size=100 // math.log2(math.sqrt(len(state))))
+    draw_table(pdf=pdf, state=state, key_position=key_position, coin_to_flip=coin_to_flip)
+    pdf.output(file_name)
+
+
+def solve(initial_state: str, coin_to_flip: int) -> str:
+    """Return the chessboard in `initial_state` after flipping `coin_to_flip`."""
+    result = list(initial_state)
+    result[coin_to_flip] = '0' if result[coin_to_flip] == '1' else '1'
+    return ''.join(result)
+
+
+def is_power_of_two(n: int) -> bool:
+    k = 1
+    while k <= n:
+        if k == n:
+            return True
+        k *= 2
+    return False
+
+
+def get_parameters(board_side: int = 8) -> Tuple[str, int, int]:
+    """Generate a random chessboard and a random key position and solve the puzzle.
+
+    Return a board of side `board_side`, a key position and the coin to flip.
+    """
+    if not is_power_of_two(board_side):
+        raise ValueError("Board side must be a power of two!")
+    random.seed()
+    initial_state = ''.join(map(str, (random.randint(0, 1) for _ in range(board_side ** 2))))
+    key_position = random.randint(0, board_side ** 2 - 1)
+    coin_to_flip = get_coin_to_flip(initial_state, key_position)
+    return initial_state, key_position, coin_to_flip
+
+
+def main() -> None:
+    board_side = 0
+    while board_side < 2:
+        try:
+            board_side = input("Choose a side length for the chessboard (press enter for default value 8)\t\t")
+            if not board_side:
+                board_side = 8
+            board_side = int(board_side)
+            if not is_power_of_two(board_side):
+                raise ValueError
+        except (ValueError, TypeError):
+            board_side = 0
+            print(f"Invalid input `{board_side}`. Please enter a power of two.")
+            continue
+        except KeyboardInterrupt:
+            print("\nExiting...")
+            return
+    print(f"Generating a random {board_side} x {board_side} chessboard...")
+    initial_state, key_position, coin_to_flip = get_parameters(board_side=board_side)
+    print("Show Player 1 the file `Key.pdf`.")
+    store_pdf(file_name='Key.pdf', state=initial_state,
+              key_position=key_position)
+    final_state = solve(initial_state, coin_to_flip)
+    print("Once Player 1 has flipped a coin, the chessboard should look like "
+          "the one in `Problem.pdf`. Show it to Player 2.")
+    store_pdf(file_name='Problem.pdf', state=final_state)
+    print("You can use `Solution.pdf` to validate the answer of Player 2.")
+    store_pdf(file_name='Solution.pdf', state=final_state,
+              key_position=key_position, coin_to_flip=coin_to_flip)
+
+
+if __name__ == '__main__':
+    main()

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+fpdf2
+Pillow~=10.0.0
+pytest~=7.4.0

+ 69 - 0
test_project.py

@@ -0,0 +1,69 @@
+import pytest
+
+from project import get_coin_to_flip, get_parameters, get_parity, is_power_of_two, solve
+
+
+def main():
+    test_get_coin_to_flip()
+    test_get_parameters()
+    test_get_parity()
+    test_is_power_of_two()
+    test_solve()
+
+
+def test_get_coin_to_flip():
+    for i in (4, 8, 16):
+        initial_state, key_position, _ = get_parameters(board_side=i)
+        c = get_coin_to_flip(initial_state=initial_state, key_position=key_position)
+        assert 0 <= c < i**2
+        final_state = solve(initial_state, c)
+        assert get_parity(final_state) == key_position
+
+
+def test_get_parameters():
+    for i in (4, 8, 16):
+        initial_state, key_position, coin_to_flip = get_parameters(board_side=i)
+        assert len(initial_state) == i ** 2
+        assert 0 <= key_position <= 2**i - 1
+        assert 0 <= coin_to_flip <= 2**i - 1
+    for i in range(3, 5, 7):
+        with pytest.raises(ValueError):
+            get_parameters(board_side=i)
+
+
+def test_get_parity():
+    assert get_parity('0100') == 1
+    assert get_parity('0010') == 2
+    assert get_parity('0' * 64) == 0
+    assert get_parity('1' * 64) == 0
+    assert get_parity('0000'
+                      '0010'
+                      '0000'
+                      '0000') == 6
+    assert get_parity('00111011'
+                      '00001001'
+                      '10010101'
+                      '11011011'
+                      '01001110'
+                      '01110000'
+                      '10001101'
+                      '11101001') == 17
+
+
+def test_is_power_of_two():
+    assert not is_power_of_two(0)
+    for i in range(1, 13):
+        assert is_power_of_two(2**i)
+        assert not is_power_of_two(2 ** i + 3)
+
+
+def test_solve():
+    for i in (4, 8, 16):
+        initial_state, _, coin_to_flip = get_parameters(board_side=i)
+        final_state = solve(initial_state=initial_state, coin_to_flip=coin_to_flip)
+        for j, (c0, c1) in enumerate(zip(initial_state, final_state)):
+            assert j == coin_to_flip or c0 == c1
+
+
+if __name__ == '__main__':
+    main()