Native pygame-ce game with fully procedural pixel art and chiptune audio (no asset files). Pure-logic physics module with flipper angular momentum, plus headless autoplay/screenshot/probe dev tools.
152 lines
5.0 KiB
Python
152 lines
5.0 KiB
Python
"""Headless autoplay test: drive the game for N frames, report health stats.
|
|
|
|
Usage: python autotest.py [frames] [seed]
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
os.environ.setdefault("SDL_VIDEODRIVER", "dummy")
|
|
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")
|
|
|
|
import pygame # noqa: E402
|
|
from game import Game # noqa: E402
|
|
|
|
|
|
class AutoPlayer:
|
|
def __init__(self, g, seed=3):
|
|
self.g = g
|
|
self.rng = __import__("random").Random(seed)
|
|
self.charge_until = 0
|
|
self.prev_events = None
|
|
self.stats = dict(launches=0, drains=0, bumpers=0, slings=0,
|
|
star_lit=0, fever=0, games=0, extra=0,
|
|
d_center=0, d_side=0)
|
|
self.prev = dict(hits=0, slingflash=0, lit=0, lives=3, next_extra=30000)
|
|
self.stuck = 0
|
|
self.prev_pos = None
|
|
|
|
def tick(self):
|
|
g = self.g
|
|
t = g.table
|
|
|
|
if g.state == "over":
|
|
g.state = "play"
|
|
g.new_game()
|
|
self.stats["games"] += 1
|
|
self.prev["lives"] = 3
|
|
self.prev["next_extra"] = 30000
|
|
|
|
# charge then release on a schedule
|
|
if g.pending == "serve" and not t.ball_live:
|
|
if self.charge_until <= 0:
|
|
self.charge_until = self.rng.randint(24, 70)
|
|
g.keys["fire"] = True
|
|
self.charge_until -= 1
|
|
if self.charge_until <= 0:
|
|
g.keys["fire"] = False
|
|
else:
|
|
g.keys["fire"] = False
|
|
self.charge_until = 0
|
|
|
|
was_live = t.ball_live
|
|
self.prev_frame = getattr(self, "prev_frame", None)
|
|
|
|
# flipper policy: catch anything heading down the middle/bottom
|
|
b = t.ball
|
|
if t.ball_live and b.vy > 0 and b.y > 150:
|
|
g.keys["L"] = b.x < 258
|
|
g.keys["R"] = b.x > 222
|
|
else:
|
|
if t.ball_live and self.rng.random() < 0.04:
|
|
g.keys["L"] = self.rng.random() < 0.5
|
|
g.keys["R"] = not g.keys["L"]
|
|
else:
|
|
g.keys["L"] = g.keys["R"] = False
|
|
|
|
g.update()
|
|
|
|
b = t.ball
|
|
if not was_live and t.ball_live:
|
|
self.stats["launches"] += 1
|
|
|
|
# count events that fired this frame (only when physics actually ran)
|
|
if t.events is not self.prev_events:
|
|
self.prev_events = t.events
|
|
for e in t.events:
|
|
if e.kind == "bumper":
|
|
self.stats["bumpers"] += 1
|
|
elif e.kind == "sling":
|
|
self.stats["slings"] += 1
|
|
elif e.kind == "star_lit":
|
|
self.stats["star_lit"] += 1
|
|
if sum(s["lit"] for s in t.stars) == len(t.stars):
|
|
self.stats["fever"] += 1
|
|
elif e.kind == "drain":
|
|
if 180 < e.x < 300:
|
|
self.stats["d_center"] += 1
|
|
else:
|
|
self.stats["d_side"] += 1
|
|
|
|
if b.speed > 0.8:
|
|
self.stuck = 0
|
|
elif t.ball_live:
|
|
self.stuck += 1
|
|
if self.stuck > 120:
|
|
print(" !! ball appears stuck at (%.0f, %.0f)" % (b.x, b.y))
|
|
self.stuck = 0
|
|
b.vx = 2
|
|
b.vy = -2
|
|
else:
|
|
self.stuck = 0
|
|
if g.lives < self.prev["lives"]:
|
|
self.stats["drains"] += self.prev["lives"] - g.lives
|
|
self.prev["lives"] = g.lives
|
|
if g.lives > self.prev["lives"]:
|
|
self.stats["extra"] += g.lives - self.prev["lives"]
|
|
self.prev["lives"] = g.lives
|
|
|
|
def report(self):
|
|
s = self.stats
|
|
print("=== AUTOPLAY REPORT ===")
|
|
print("games played :", s["games"])
|
|
print("launches :", s["launches"])
|
|
print("drains :", s["drains"])
|
|
print("bumper hits :", s["bumpers"])
|
|
print("sling hits :", s["slings"])
|
|
print("stars lit :", s["star_lit"])
|
|
print("fevers :", s["fever"])
|
|
print("extra lives :", s["extra"])
|
|
print("final score :", self.g.score)
|
|
print("hi :", self.g.hi)
|
|
print("avg drain/ball ok" if s["drains"] > 0 else "!! NO DRAINS SEEN")
|
|
print("drains: center=%d side=%d" % (s["d_center"], s["d_side"]))
|
|
if s["bumpers"] == 0:
|
|
print("!! BUMPERS NEVER HIT")
|
|
if s["slings"] == 0:
|
|
print("!! SLINGS NEVER HIT")
|
|
if s["star_lit"] == 0:
|
|
print("!! STARS NEVER LIT")
|
|
return (s["bumpers"], s["slings"], s["star_lit"], s["drains"])
|
|
|
|
|
|
def main():
|
|
frames = int(sys.argv[1]) if len(sys.argv) > 1 else 40000
|
|
seed = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
|
g = Game()
|
|
g.state = "play"
|
|
g.new_game()
|
|
ap = AutoPlayer(g, seed)
|
|
clock = pygame.time.Clock()
|
|
for i in range(frames):
|
|
ap.tick()
|
|
if i % 2000 == 0:
|
|
print("frame %d score=%d lives=%d ball_live=%s pending=%s" %
|
|
(i, g.score, g.lives, g.table.ball_live, g.pending))
|
|
ap.report()
|
|
pygame.quit()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|