Add integration screenshots and screenshot tooling
Screenshots generated via Playwright showing key game states: - 01_midgame_properties_owned: Properties with colored owner indicators - 02_houses_built: Houses/hotels rendered on properties - 03_after_trade: Board state after a property trade - 04_baltic_mortgaged: Mortgaged property display - 05_bob_bankrupt: Bankrupt player with skull/opacity/strikethrough - 06_game_over: Game over with winner confetti Also includes earlier QA screenshots: - single_player_joined: Lobby with one player registered - player_bankrupt_game_over: Bankrupt endgame state Tools: screenshot_states.py (synthetic states) and screenshot_integration.py (integration test scenarios)
234
screenshot_integration.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Take screenshots at key moments during a synthetic game that exercises all fixed bugs.
|
||||
Writes game-state.json at each step and captures the UI.
|
||||
"""
|
||||
import json, os, sys, time, subprocess
|
||||
from datetime import datetime, timezone
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from monop_parser import MonopParser, BOARD
|
||||
|
||||
SITE_DIR = os.path.join(os.path.dirname(__file__), "site")
|
||||
STATE_PATH = os.path.join(SITE_DIR, "game-state.json")
|
||||
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
|
||||
os.makedirs(SCREENSHOTS_DIR, exist_ok=True)
|
||||
|
||||
_t = [0]
|
||||
def ts():
|
||||
_t[0] += 1
|
||||
return f"2026-01-01 00:{_t[0]//60:02d}:{_t[0]%60:02d}"
|
||||
|
||||
def feed(p, lines):
|
||||
for line in lines:
|
||||
p.parse_line(f"{ts()}\t{line}")
|
||||
|
||||
def write_state(p):
|
||||
state = p.get_state()
|
||||
state["lastUpdated"] = datetime.now(timezone.utc).isoformat()
|
||||
with open(STATE_PATH, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
def screenshot(page, name, wait=3000):
|
||||
page.wait_for_timeout(wait)
|
||||
path = os.path.join(SCREENSHOTS_DIR, f"{name}.png")
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f"[screenshot] {name}")
|
||||
|
||||
def main():
|
||||
server = subprocess.Popen(
|
||||
[sys.executable, "-m", "http.server", "9998", "--directory", SITE_DIR],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch()
|
||||
page = browser.new_page(viewport={"width": 1280, "height": 1600})
|
||||
|
||||
p = MonopParser()
|
||||
|
||||
# === Setup: 3-player game ===
|
||||
feed(p, [
|
||||
"monop\tHow many players? ",
|
||||
"monop\tPlayer 1, say 'me' please.",
|
||||
"monop\talice (1) rolls 3",
|
||||
"monop\tPlayer 2, say 'me' please.",
|
||||
"monop\tbob (2) rolls 5",
|
||||
"monop\tPlayer 3, say 'me' please.",
|
||||
"monop\tcharlie (3) rolls 9",
|
||||
"monop\tcharlie (3) goes first",
|
||||
"monop\tcharlie (3) (cash $1500) on === GO ===",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
|
||||
# charlie buys Mediterranean + Oriental, gets lightblue monopoly
|
||||
feed(p, [
|
||||
"charlie\t.",
|
||||
"monop\troll is 1, 0",
|
||||
"monop\tThat puts you on Mediterranean ave. (P)",
|
||||
"monop\tThat would cost $60",
|
||||
"monop\tDo you want to buy? ",
|
||||
"charlie\t.y",
|
||||
"monop\talice (1) (cash $1500) on === GO ===",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
g = p.game
|
||||
g.property_owner[6] = 3 # Oriental
|
||||
g.property_owner[8] = 3 # Vermont
|
||||
g.property_owner[9] = 3 # Connecticut
|
||||
|
||||
# alice buys Baltic, bob buys Reading RR + B&O RR
|
||||
feed(p, [
|
||||
"alice\t.",
|
||||
"monop\troll is 1, 2",
|
||||
"monop\tThat puts you on Baltic ave. (P)",
|
||||
"monop\tThat would cost $60",
|
||||
"monop\tDo you want to buy? ",
|
||||
"alice\t.y",
|
||||
"monop\tbob (2) (cash $1500) on === GO ===",
|
||||
"monop\t-- Command: ",
|
||||
"bob\t.",
|
||||
"monop\troll is 2, 3",
|
||||
"monop\tThat puts you on Reading RR",
|
||||
"monop\tThat would cost $200",
|
||||
"monop\tDo you want to buy? ",
|
||||
"bob\t.y",
|
||||
"monop\tcharlie (3) (cash $1440) on Mediterranean ave. (P)",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
g.property_owner[25] = 2 # B&O RR for bob
|
||||
|
||||
# === Screenshot 1: Mid-game with properties owned ===
|
||||
write_state(p)
|
||||
page.goto("http://localhost:9998")
|
||||
screenshot(page, "01_midgame_properties_owned")
|
||||
|
||||
# === charlie buys houses (House sub-parser) ===
|
||||
feed(p, [
|
||||
"charlie\t.buy",
|
||||
"monop\tOriental ave. (L) (0) Vermont ave. (L) (0) Connecticut ave. (L) (0) ",
|
||||
"monop\tHouses will cost $50",
|
||||
"monop\tHow many houses do you wish to buy for",
|
||||
"monop\tOriental ave. (L) (0): ",
|
||||
"charlie\t.3",
|
||||
"monop\tVermont ave. (L) (0): ",
|
||||
"charlie\t.3",
|
||||
"monop\tConnecticut ave. (L) (0): ",
|
||||
"charlie\t.3",
|
||||
"monop\tYou asked for 9 houses for $450",
|
||||
"monop\tIs that ok? ",
|
||||
"charlie\t.y",
|
||||
"monop\tcharlie (3) (cash $990) on Mediterranean ave. (P)",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
|
||||
# === Screenshot 2: Houses visible on board ===
|
||||
write_state(p)
|
||||
page.reload()
|
||||
screenshot(page, "02_houses_built")
|
||||
|
||||
# === Trade: bob gives Reading RR to charlie for $300 ===
|
||||
feed(p, [
|
||||
"monop\talice (1) (cash $1440) on Baltic ave. (P)",
|
||||
"monop\t-- Command: ",
|
||||
"alice\t.",
|
||||
"monop\troll is 2, 1",
|
||||
"monop\tThat puts you on Oriental ave. (L)",
|
||||
"monop\tOwned by charlie",
|
||||
"monop\twith 3 houses, rent is 90",
|
||||
"monop\tbob (2) (cash $1300) on Reading RR",
|
||||
"monop\t-- Command: ",
|
||||
"monop\tPlayer bob (2) gives:",
|
||||
"monop\t Reading RR 2 200 1",
|
||||
"monop\tPlayer charlie (3) gives:",
|
||||
"monop\t $300",
|
||||
"monop\tcharlie, is the trade ok? ",
|
||||
"charlie\t.y",
|
||||
"monop\tTrade is done!",
|
||||
"monop\tbob (2) (cash $1600) on Reading RR",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
|
||||
# === Screenshot 3: After trade — Reading RR now charlie's ===
|
||||
write_state(p)
|
||||
page.reload()
|
||||
screenshot(page, "03_after_trade")
|
||||
|
||||
# === alice mortgages Baltic ===
|
||||
feed(p, [
|
||||
"bob\t.",
|
||||
"monop\troll is 1, 2",
|
||||
"monop\tThat puts you on Vermont ave. (L)",
|
||||
"monop\tOwned by charlie",
|
||||
"monop\twith 3 houses, rent is 90",
|
||||
"monop\talice (1) (cash $1350) on Oriental ave. (L)",
|
||||
"monop\t-- Command: ",
|
||||
"alice\t.mor",
|
||||
"monop\tWhich property do you want to mortgage? ",
|
||||
"alice\t.baltic",
|
||||
"monop\tThat got you $30",
|
||||
"monop\talice (1) (cash $1380) on Oriental ave. (L)",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
|
||||
# === Screenshot 4: Baltic mortgaged ===
|
||||
write_state(p)
|
||||
page.reload()
|
||||
screenshot(page, "04_baltic_mortgaged")
|
||||
|
||||
# === bob resigns to bank ===
|
||||
feed(p, [
|
||||
"alice\t.",
|
||||
"monop\troll is 1, 3",
|
||||
"monop\tThat puts you on Connecticut ave. (L)",
|
||||
"monop\tOwned by charlie",
|
||||
"monop\twith 3 houses, rent is 90",
|
||||
"monop\tbob (2) (cash $1510) on Vermont ave. (L)",
|
||||
"monop\t-- Command: ",
|
||||
"bob\t.resign",
|
||||
"monop\tWho do you wish to resign to? ",
|
||||
"bob\t.bank",
|
||||
"monop\tDo you really want to resign? ",
|
||||
"bob\t.y",
|
||||
"monop\tresigning to bank",
|
||||
"monop\tcharlie (1) (cash $1380) on Mediterranean ave. (P)",
|
||||
"monop\t-- Command: ",
|
||||
])
|
||||
|
||||
# === Screenshot 5: Bob bankrupt ===
|
||||
write_state(p)
|
||||
page.reload()
|
||||
screenshot(page, "05_bob_bankrupt")
|
||||
|
||||
# === alice resigns to charlie — game over ===
|
||||
feed(p, [
|
||||
"charlie\t.",
|
||||
"monop\troll is 2, 3",
|
||||
"monop\tThat puts you on Oriental ave. (L)",
|
||||
"monop\tYou own it.",
|
||||
"monop\talice (1) (cash $1260) on Connecticut ave. (L)",
|
||||
"monop\t-- Command: ",
|
||||
"monop\tYou would resign to charlie",
|
||||
"monop\tDo you really want to resign? ",
|
||||
"alice\t.y",
|
||||
"monop\tresigning to player",
|
||||
"monop\tTrade is done!",
|
||||
"monop\tThen charlie WINS!!!!!",
|
||||
])
|
||||
|
||||
# === Screenshot 6: Game over ===
|
||||
write_state(p)
|
||||
page.reload()
|
||||
screenshot(page, "06_game_over")
|
||||
|
||||
browser.close()
|
||||
print("\nAll screenshots captured!")
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
90
screenshot_states.py
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Take screenshots of specific game states by writing synthetic game-state.json."""
|
||||
import json, os, sys, time, subprocess
|
||||
from datetime import datetime, timezone
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
SITE_DIR = os.path.join(os.path.dirname(__file__), "site")
|
||||
STATE_PATH = os.path.join(SITE_DIR, "game-state.json")
|
||||
SCREENSHOTS_DIR = os.path.join(os.path.dirname(__file__), "screenshots")
|
||||
os.makedirs(SCREENSHOTS_DIR, exist_ok=True)
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from monop_parser import MonopParser
|
||||
_p = MonopParser()
|
||||
_p.parse_line("2026-01-01 00:00:00\tmonop\tHow many players? ")
|
||||
_p.parse_line("2026-01-01 00:00:00\talice\t3")
|
||||
SQUARES = _p.get_state()["squares"]
|
||||
|
||||
def make_player(name, number, money, location, properties=None, inJail=False, bankrupt=False):
|
||||
d = {
|
||||
"name": name, "number": number, "money": money, "location": location,
|
||||
"inJail": inJail, "jailTurns": 0, "doublesCount": 0,
|
||||
"getOutOfJailFreeCards": 0, "properties": properties or [],
|
||||
}
|
||||
if bankrupt:
|
||||
d["bankrupt"] = True
|
||||
return d
|
||||
|
||||
def write_state(state):
|
||||
state["lastUpdated"] = datetime.now(timezone.utc).isoformat()
|
||||
state.setdefault("squares", SQUARES)
|
||||
with open(STATE_PATH, "w") as f:
|
||||
json.dump(state, f, indent=2)
|
||||
|
||||
def take_screenshot(page, name):
|
||||
page.goto("http://localhost:9998")
|
||||
page.wait_for_timeout(4000)
|
||||
path = os.path.join(SCREENSHOTS_DIR, f"{name}.png")
|
||||
page.screenshot(path=path, full_page=True)
|
||||
print(f"[screenshot] {path}")
|
||||
|
||||
def main():
|
||||
server = subprocess.Popen(
|
||||
[sys.executable, "-m", "http.server", "9998", "--directory", SITE_DIR],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page(viewport={"width": 1280, "height": 1600})
|
||||
|
||||
# 1: Single player joined during setup
|
||||
write_state({
|
||||
"players": [make_player("alice", 1, 1500, 0)],
|
||||
"currentPlayer": None,
|
||||
"log": [
|
||||
{"text": "Game for 3 players", "player": None, "timestamp": "2026-01-01 00:00:00"},
|
||||
{"text": "alice joined the game", "player": "alice", "timestamp": "2026-01-01 00:00:01"},
|
||||
],
|
||||
"phase": "setup",
|
||||
"numPlayersExpected": 3,
|
||||
})
|
||||
take_screenshot(page, "single_player_joined")
|
||||
|
||||
# 2: Game over — players bankrupt
|
||||
write_state({
|
||||
"players": [
|
||||
make_player("alice", 1, 4200, 24, [1, 3, 5, 11, 13, 14, 15, 21, 23, 24, 25, 37, 39]),
|
||||
make_player("bob", 2, 0, 18, bankrupt=True),
|
||||
make_player("charlie", 3, 0, 6, bankrupt=True),
|
||||
],
|
||||
"currentPlayer": 1,
|
||||
"log": [
|
||||
{"text": "bob is bankrupt!", "player": "bob", "timestamp": "2026-01-01 01:30:00"},
|
||||
{"text": "charlie is bankrupt!", "player": "charlie", "timestamp": "2026-01-01 01:45:00"},
|
||||
{"text": "alice WINS!", "player": "alice", "timestamp": "2026-01-01 01:45:01"},
|
||||
],
|
||||
"phase": "over",
|
||||
})
|
||||
take_screenshot(page, "player_bankrupt_game_over")
|
||||
|
||||
browser.close()
|
||||
finally:
|
||||
server.terminate()
|
||||
server.wait()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
screenshots/01_midgame_properties_owned.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
screenshots/02_houses_built.png
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
screenshots/03_after_trade.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
screenshots/04_baltic_mortgaged.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
screenshots/05_bob_bankrupt.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
screenshots/06_game_over.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
screenshots/player_bankrupt_game_over.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
screenshots/single_player_joined.png
Normal file
|
After Width: | Height: | Size: 70 KiB |