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)
This commit is contained in:
Jarvis 2026-02-21 20:01:57 +00:00
parent 66412a5c67
commit bdf14a7c58
10 changed files with 324 additions and 0 deletions

234
screenshot_integration.py Normal file
View 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
View 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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB