Final project: chessboard puzzle game

This commit is contained in:
Davte 2023-07-09 21:17:00 +02:00
parent 63d06d6b35
commit 17075632d0
Signed by: Davte
GPG Key ID: 70336F92E6814706
4 changed files with 241 additions and 2 deletions

View File

@ -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
project.py Normal file
View File

@ -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
requirements.txt Normal file
View File

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

69
test_project.py Normal file
View File

@ -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()