Final project: chessboard puzzle game
This commit is contained in:
parent
63d06d6b35
commit
17075632d0
45
README.md
45
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
project.py
Normal file
126
project.py
Normal 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
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
fpdf2
|
||||
Pillow~=10.0.0
|
||||
pytest~=7.4.0
|
69
test_project.py
Normal file
69
test_project.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user