Portrait 360x480 table with regular-sized flippers
Rotate the playfield to portrait like a real pinball machine: - canvas 480x360 landscape -> 360x480 portrait; playfield 308px wide, ~390px tall, with HUD, title/game-over screens and HUD re-laid out for the narrow format (score/best top row, balls/stars/fever below) - flippers reduced to a regular ~quarter-width size (82px) pivoting at the lower corners, tips meeting over a wide centre drain - table elements re-placed for the taller field: 3 star lanes up top, candy bumpers mid-field, slingshots and floor-level outlane guard posts at the bottom; data-checked with the autoplay bot (~350 bumper hits / 30k frames, centre+side drains, no stuck balls) - probes & autoplay policy updated for the portrait geometry; all 13 pixel-probes pass
@@ -6,8 +6,8 @@ audio asset files**: every sprite, texture, particle, chiptune and sound
|
||||
effect is generated by code at runtime.
|
||||
|
||||
Not web-based: it opens its own native window and renders on a low-res
|
||||
`480×360` virtual canvas, upscaled with crisp nearest-neighbour scaling for an
|
||||
authentic retro look.
|
||||
**portrait** `360×480` virtual canvas — like a proper pinball machine —
|
||||
upscaled with crisp nearest-neighbour scaling for an authentic retro look.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ class AutoPlayer:
|
||||
|
||||
# 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
|
||||
if t.ball_live and b.vy > 0 and b.y > 200:
|
||||
g.keys["L"] = b.x < 196
|
||||
g.keys["R"] = b.x > 164
|
||||
else:
|
||||
if t.ball_live and self.rng.random() < 0.04:
|
||||
g.keys["L"] = self.rng.random() < 0.5
|
||||
|
||||
@@ -9,7 +9,7 @@ import pygame
|
||||
from physics import (CV_W, CV_H, PLAY_L, PLAY_R, PLAY_T, PLAY_B, BALL_R,
|
||||
CENTER_HOLE, OUTLANE_L, OUTLANE_R, DRAIN_Y, LAUNCH_DIR,
|
||||
LAUNCH_MIN, LAUNCH_MAX, Table, FLIP_PIVOT_L, FLIP_PIVOT_R,
|
||||
FLIP_RADIUS, FLIP_LEN, flipper_tip)
|
||||
FLIP_RADIUS, FLIP_LEN, flipper_tip, BUMPERS)
|
||||
import font as pix
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -362,9 +362,9 @@ class Game:
|
||||
t = y / CV_H
|
||||
col = tuple(int(a + (b - a) * t) for a, b in zip(C["bg_top"], C["bg_bot"]))
|
||||
pygame.draw.line(bg, col, (0, y), (CV_W, y))
|
||||
# twinkle stars & planets for the background
|
||||
# twinkle stars & planets around the table, drawn over the HUD & margins
|
||||
for sx, sy, tw, ph in self.stars_bg:
|
||||
if 0 < sx < 20 or 460 < sx < CV_W or sy < 46:
|
||||
if sx < PLAY_L - 6 or sx > PLAY_R + 6 or sy < 50:
|
||||
pygame.draw.rect(bg, (230, 225, 255), (sx, sy, 1, 1))
|
||||
self.bg = bg
|
||||
|
||||
@@ -382,12 +382,11 @@ class Game:
|
||||
PLAY_R - PLAY_L - 8, PLAY_B - PLAY_T - 8), border_radius=5)
|
||||
# candy dot sparkle inside the field
|
||||
r = self.rng
|
||||
for _ in range(46):
|
||||
for _ in range(60):
|
||||
dx = r.randrange(int(PLAY_L) + 12, int(PLAY_R) - 12)
|
||||
dy = r.randrange(int(PLAY_T) + 14, int(PLAY_B) - 12)
|
||||
if any((dx - b["pos"][0]) ** 2 + (dy - b["pos"][1]) ** 2 < (b["r"] + 4) ** 2
|
||||
for b in ({"pos": (150, 180), "r": 23}, {"pos": (330, 180), "r": 23},
|
||||
{"pos": (240, 240), "r": 23})):
|
||||
for b in BUMPERS):
|
||||
continue
|
||||
col = PASTELS[r.randrange(len(PASTELS))]
|
||||
field.fill(tuple(c // 3 for c in col), (dx, dy, 2, 2))
|
||||
@@ -412,8 +411,11 @@ class Game:
|
||||
hud.fill(C["hud"])
|
||||
pygame.draw.line(hud, C["rail_dark"], (0, 50), (CV_W, 50), 2)
|
||||
pygame.draw.line(hud, C["hud_line"], (0, 47), (CV_W, 47), 1)
|
||||
pygame.draw.rect(hud, (255, 255, 255), (0, 0, 8, 52))
|
||||
pygame.draw.rect(hud, (255, 255, 255), (CV_W - 8, 0, 8, 52))
|
||||
# static labels
|
||||
pix.draw_text(hud, 10, 4, "SCORE", C["dim"], 1)
|
||||
pix.draw_text(hud, CV_W - 10, 4, "BEST", C["dim"], 1, align="right")
|
||||
pix.draw_text(hud, 10, 33, "BALLS", C["dim"], 1)
|
||||
pix.draw_text(hud, 120, 33, "STARS", C["dim"], 1)
|
||||
self.hud = hud
|
||||
|
||||
# ------------------------------------------------------------------ game
|
||||
@@ -471,8 +473,8 @@ class Game:
|
||||
self.sfx.play("fever")
|
||||
self.cat_say("CANDY FEVER!", 90, "star")
|
||||
self.shake = 3
|
||||
self.add_msg("CANDY FEVER!", CV_W // 2, 150, C["gold"], scale=2)
|
||||
self.add_msg("DOUBLE SCORE x2", CV_W // 2, 166, C["pink"], scale=1)
|
||||
self.add_msg("CANDY FEVER!", CV_W // 2, 210, C["gold"], scale=2)
|
||||
self.add_msg("DOUBLE SCORE x2", CV_W // 2, 226, C["pink"], scale=1)
|
||||
for i in range(30):
|
||||
self.particles.append(dict(
|
||||
x=self.rng.uniform(PLAY_L + 10, PLAY_R - 10), y=self.rng.uniform(60, 120),
|
||||
@@ -645,7 +647,7 @@ class Game:
|
||||
self.lives += 1
|
||||
self.sfx.play("extra")
|
||||
self.cat_say("EXTRA BALL!", 90, "star")
|
||||
self.add_msg("EXTRA BALL!", CV_W // 2, 120, C["pink"], scale=2)
|
||||
self.add_msg("EXTRA BALL!", CV_W // 2, 200, C["pink"], scale=2)
|
||||
self.next_extra += 30000
|
||||
|
||||
def add_msg(self, text, x, y, color, scale=1):
|
||||
@@ -741,7 +743,7 @@ class Game:
|
||||
# combo indicator
|
||||
if self.combo >= 2 and self.pending == "live":
|
||||
col = (255, 140, 90) if self.combo >= 5 else C["gold"]
|
||||
pix.draw_text(c, CV_W // 2, 56, "COMBO x%d!" % self.combo, col, 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 68, "COMBO x%d!" % self.combo, col, 1, align="center")
|
||||
|
||||
# particles
|
||||
for p in self.particles:
|
||||
@@ -772,21 +774,21 @@ class Game:
|
||||
# pending banners
|
||||
if self.pending == "ball_banner" and self.pending_t > 40:
|
||||
self._dim_center(c)
|
||||
pix.draw_text(c, CV_W // 2, 140, "BALL %d" % self.ball_no, C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 170, "GET READY...", C["pink"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 255, "BALL %d" % self.ball_no, C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 300, "GET READY...", C["pink"], 1, align="center")
|
||||
if self.pending == "drain_pause":
|
||||
self._dim_center(c)
|
||||
pix.draw_text(c, CV_W // 2, 150, "LOST THE BALL!", C["pink"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 285, "LOST THE BALL!", C["pink"], 2, align="center")
|
||||
if self.pending == "serve" and not t.ball_live:
|
||||
pix.draw_text(c, CV_W // 2, 300, "HOLD SPACE / UP TO CHARGE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 372, "HOLD SPACE / UP TO CHARGE", C["dim"], 1, align="center")
|
||||
|
||||
# HUD
|
||||
self._render_hud(c)
|
||||
|
||||
def _dim_center(self, c):
|
||||
self.overlay.fill((0, 0, 0, 0))
|
||||
pygame.draw.rect(self.overlay, (12, 8, 40, 160),
|
||||
(CV_W // 2 - 100, 110, 200, 90), border_radius=8)
|
||||
pygame.draw.rect(self.overlay, (12, 8, 40, 170),
|
||||
(CV_W // 2 - 100, 230, 200, 120), border_radius=10)
|
||||
c.blit(self.overlay, (0, 0))
|
||||
|
||||
def _draw_bubble(self, c):
|
||||
@@ -801,29 +803,28 @@ class Game:
|
||||
|
||||
def _render_hud(self, c):
|
||||
c.blit(self.hud, (0, 0))
|
||||
pix.draw_text(c, 14, 4, "SCORE", C["dim"], 1)
|
||||
pix.draw_text(c, 14, 13, "{:,}".format(self.score), C["white"], 2)
|
||||
pix.draw_text(c, 170, 4, "BEST", C["dim"], 1)
|
||||
pix.draw_text(c, 10, 13, "{:,}".format(self.score), C["white"], 2)
|
||||
best = max(self.hi, self.score)
|
||||
pix.draw_text(c, 170, 13, "{:,}".format(best), C["gold"], 2)
|
||||
# lives hearts
|
||||
for i in range(self.lives):
|
||||
draw_heart(c, 336 + i * 14, 14, 2, C["heart"])
|
||||
pix.draw_text(c, 336, 30, "LIVES", C["dim"], 1)
|
||||
# stars progress
|
||||
pix.draw_text(c, 14, 32, "STARS", C["dim"], 1)
|
||||
pix.draw_text(c, CV_W - 10, 13, "{:,}".format(best), C["gold"], 2, align="right")
|
||||
# lives hearts (cap the icon row, spill into a +N text)
|
||||
hearts = self.lives
|
||||
for i in range(min(hearts, 5)):
|
||||
draw_heart(c, 10 + i * 14, 42, 2, C["heart"])
|
||||
if hearts > 5:
|
||||
pix.draw_text(c, 10 + 5 * 14 + 2, 43, "+%d" % (hearts - 5), C["pink"], 1)
|
||||
# star progress
|
||||
for i, st in enumerate(self.table.stars):
|
||||
x = 52 + i * 18
|
||||
draw_star(c, x, 37, 5 if st["lit"] else 4,
|
||||
x = 120 + i * 16
|
||||
draw_star(c, x + 4, 42, 5 if st["lit"] else 4,
|
||||
C["gold"] if st["lit"] else (90, 78, 150))
|
||||
# fever indicator + bar
|
||||
if self.fever > 0:
|
||||
frac = max(0.0, self.fever / (60 * 20))
|
||||
pygame.draw.rect(c, (60, 46, 110), (260, 30, 70, 6))
|
||||
pygame.draw.rect(c, (255, 150, 200), (260, 30, int(70 * frac), 6))
|
||||
pix.draw_text(c, 260, 38, "FEVER x2", C["gold"], 1)
|
||||
pix.draw_text(c, 205, 33, "FEVER", C["gold"], 1)
|
||||
pygame.draw.rect(c, (60, 46, 110), (205, 44, 90, 5))
|
||||
pygame.draw.rect(c, (255, 150, 200), (205, 44, int(90 * frac), 5))
|
||||
if self.paused:
|
||||
pix.draw_text(c, CV_W - 40, 40, "PAUSED", C["dim"], 1, align="right")
|
||||
pix.draw_text(c, CV_W - 10, 40, "PAUSED", C["dim"], 1, align="right")
|
||||
|
||||
# ------------------------------------------------------------------ title
|
||||
def _title_update(self):
|
||||
@@ -837,12 +838,12 @@ class Game:
|
||||
|
||||
def _render_title(self, c):
|
||||
c.blit(self.bg, (0, 0))
|
||||
# ringed planet
|
||||
pygame.draw.circle(c, (150, 110, 220), (90, 70), 24)
|
||||
pygame.draw.circle(c, (190, 160, 245), (90, 70), 24, 2)
|
||||
pygame.draw.ellipse(c, (255, 200, 220), (58, 64, 64, 18), 2)
|
||||
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10)
|
||||
pygame.draw.circle(c, (110, 90, 200), (300, 320), 10, 1)
|
||||
# ringed planet top-left, little moon bottom-right
|
||||
pygame.draw.circle(c, (150, 110, 220), (62, 80), 22)
|
||||
pygame.draw.circle(c, (190, 160, 245), (62, 80), 22, 2)
|
||||
pygame.draw.ellipse(c, (255, 200, 220), (34, 74, 56, 16), 2)
|
||||
pygame.draw.circle(c, (110, 90, 200), (310, 380), 12)
|
||||
pygame.draw.circle(c, (150, 120, 230), (310, 380), 12, 1)
|
||||
# twinkling stars
|
||||
for sx, sy, tw, ph in self.stars_bg:
|
||||
if (self.frame + ph) % 70 < 6:
|
||||
@@ -852,57 +853,55 @@ class Game:
|
||||
c.fill(p["color"], (int(p["x"]), int(p["y"]), 2, 2))
|
||||
|
||||
# logo
|
||||
words = [("COSMIC", 3, 36, C["pink"]), ("CANDY", 3, 62, C["gold"]),
|
||||
("PINBALL", 2, 108, C["mint"])]
|
||||
y = 40
|
||||
for txt, sc, _, col in words:
|
||||
y = 44
|
||||
for txt, sc, col in (("COSMIC", 3, C["pink"]), ("CANDY", 3, C["gold"]),
|
||||
("PINBALL", 2, C["mint"])):
|
||||
tw = pix.text_width(txt, sc)
|
||||
x = CV_W // 2 - tw // 2
|
||||
for i, ch in enumerate(txt):
|
||||
col2 = col
|
||||
pix.draw_text(c, x, y, ch, col2, sc, shadow=(20, 12, 50))
|
||||
pix.draw_text(c, x, y, ch, col, sc, shadow=(20, 12, 50))
|
||||
x += (5 + 1) * sc
|
||||
y += (5 + 2) * sc
|
||||
# little shooting star occasionally
|
||||
if self.frame % 90 < 24:
|
||||
t0 = self.frame % 90
|
||||
sx = 330 + t0 * 3
|
||||
sy = 40 + t0 * 1.5
|
||||
sx = 250 + t0 * 2
|
||||
sy = 130 + t0
|
||||
pygame.draw.line(c, (255, 255, 255), (int(sx), int(sy)), (int(sx - 8), int(sy - 4)), 1)
|
||||
|
||||
# cat
|
||||
draw_body(c, CV_W // 2 - 26, 180, 2)
|
||||
draw_cat(c, CV_W // 2 - 26, 168, 2, "happy", self.frame)
|
||||
draw_body(c, CV_W // 2 - 24, 208, 2)
|
||||
draw_cat(c, CV_W // 2 - 24, 196, 2, "happy", self.frame)
|
||||
# instructions panel
|
||||
pygame.draw.rect(c, (22, 15, 56), (70, 240, CV_W - 140, 74), border_radius=8)
|
||||
pygame.draw.rect(c, C["rail"], (70, 240, CV_W - 140, 74), 2, border_radius=8)
|
||||
pygame.draw.rect(c, (22, 15, 56), (30, 262, CV_W - 60, 84), border_radius=8)
|
||||
pygame.draw.rect(c, C["rail"], (30, 262, CV_W - 60, 84), 2, border_radius=8)
|
||||
lines = [
|
||||
("LEFT / RIGHT ARROWS = FLIPPERS", C["flipper"]),
|
||||
("HOLD SPACE OR UP = CHARGE CANNON", C["gold"]),
|
||||
("M MUTE R RESTART P PAUSE", C["dim"]),
|
||||
]
|
||||
for i, (txt, col) in enumerate(lines):
|
||||
pix.draw_text(c, CV_W // 2, 250 + i * 20, txt, col, 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 272 + i * 20, txt, col, 1, align="center")
|
||||
|
||||
if (self.frame // 30) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 325, "PRESS ENTER TO START", C["white"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 378, "PRESS ENTER TO START", C["white"], 2, align="center")
|
||||
|
||||
# ------------------------------------------------------------------ game over
|
||||
def _render_over(self, c):
|
||||
self._render_play(c)
|
||||
self.overlay.fill((14, 9, 40, 215))
|
||||
c.blit(self.overlay, (0, 0))
|
||||
pix.draw_text(c, CV_W // 2, 60, "GAME OVER", C["pink"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 105, "SCORE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 115, "{:,}".format(self.score), C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 160, "BEST", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 170, "{:,}".format(self.hi), C["gold"], 2, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 70, "GAME OVER", C["pink"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 128, "SCORE", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 140, "{:,}".format(self.score), C["white"], 3, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 195, "BEST", C["dim"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 207, "{:,}".format(self.hi), C["gold"], 2, align="center")
|
||||
if self.new_hi and (self.frame // 20) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 205, "NEW HI-SCORE!", C["gold"], 1, align="center")
|
||||
draw_body(c, CV_W // 2 - 20, 240, 2)
|
||||
draw_cat(c, CV_W // 2 - 20, 226, 2, "sad", self.frame)
|
||||
pix.draw_text(c, CV_W // 2, 250, "NEW HI-SCORE!", C["gold"], 1, align="center")
|
||||
draw_body(c, CV_W // 2 - 20, 310, 2)
|
||||
draw_cat(c, CV_W // 2 - 20, 296, 2, "sad", self.frame)
|
||||
if (self.frame // 30) % 2 == 0:
|
||||
pix.draw_text(c, CV_W // 2, 320, "PRESS ENTER TO PLAY AGAIN", C["white"], 1, align="center")
|
||||
pix.draw_text(c, CV_W // 2, 400, "PRESS ENTER TO PLAY AGAIN", C["white"], 1, align="center")
|
||||
|
||||
# ------------------------------------------------------------------ input
|
||||
def event(self, ev):
|
||||
|
||||
@@ -1,60 +1,65 @@
|
||||
"""Pinball physics: pure logic, no rendering. Testable headless.
|
||||
|
||||
Coordinate system: y grows downward. 480x360 virtual canvas.
|
||||
Coordinate system: y grows downward. 360x480 portrait virtual canvas.
|
||||
"""
|
||||
|
||||
import math
|
||||
import random
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Playfield geometry
|
||||
# Playfield geometry (portrait - like a proper pinball machine)
|
||||
# ---------------------------------------------------------------------------
|
||||
CV_W, CV_H = 480, 360
|
||||
PLAY_L, PLAY_R, PLAY_T, PLAY_B = 20.0, 460.0, 52.0, 346.0
|
||||
CV_W, CV_H = 360, 480
|
||||
PLAY_L, PLAY_R, PLAY_T, PLAY_B = 26.0, 334.0, 60.0, 448.0
|
||||
|
||||
GRAVITY = 0.24 # px/frame^2
|
||||
MAX_SPEED = 13.0 # speed cap (prevents tunneling; ~300px of rise)
|
||||
RESTITUTION = 0.42
|
||||
BALL_R = 7.0
|
||||
|
||||
FLIP_LEN = 154.0
|
||||
FLIP_REST = math.radians(6.0) # tips slightly lowered toward center drain
|
||||
FLIP_UP = -math.radians(52.0) # fired position
|
||||
FLIP_PIVOT_L = (62.0, 338.0)
|
||||
FLIP_PIVOT_R = (418.0, 338.0)
|
||||
FLIP_RADIUS = 5.5
|
||||
# regular-sized flippers (about a quarter of the playfield width each),
|
||||
# pivoting near the lower corners with their tips meeting at the centre
|
||||
FLIP_LEN = 82.0
|
||||
FLIP_REST = math.radians(5.0) # tips slightly lowered toward the drain
|
||||
FLIP_UP = -math.radians(48.0) # fired position
|
||||
FLIP_PIVOT_L = (78.0, 430.0)
|
||||
FLIP_PIVOT_R = (282.0, 430.0)
|
||||
FLIP_RADIUS = 5.0
|
||||
FLIP_SNAP_UP = 0.36 # rad/frame while firing
|
||||
FLIP_SNAP_DOWN = 0.05 # gentle return speed
|
||||
FLIP_BOUNCE = 0.12
|
||||
|
||||
# elements
|
||||
BUMPERS = [
|
||||
dict(pos=(150.0, 180.0), r=23.0, color="pink"),
|
||||
dict(pos=(330.0, 180.0), r=23.0, color="mint"),
|
||||
dict(pos=(240.0, 240.0), r=23.0, color="lemon"),
|
||||
dict(pos=(102.0, 205.0), r=22.0, color="pink"),
|
||||
dict(pos=(258.0, 205.0), r=22.0, color="mint"),
|
||||
dict(pos=(180.0, 300.0), r=22.0, color="lemon"),
|
||||
]
|
||||
SLING_KICK_L = (0.60, -0.80)
|
||||
SLING_KICK_R = (-0.60, -0.80)
|
||||
SLINGS = [
|
||||
# upper slingshots just below the two outer bumpers - kick toward centre
|
||||
dict(pos=(150.0, 282.0), r=11.0, kick=SLING_KICK_L),
|
||||
dict(pos=(330.0, 282.0), r=11.0, kick=SLING_KICK_R),
|
||||
# guard posts at the outlane mouths: wall-huggers get bounced back into
|
||||
# play instead of silently draining - the centre gap is the real risk
|
||||
dict(pos=(44.0, 314.0), r=12.0, kick=(0.80, -0.60)),
|
||||
dict(pos=(436.0, 314.0), r=12.0, kick=(-0.80, -0.60)),
|
||||
# slingshots in the lower side areas - kick toward the centre
|
||||
dict(pos=(92.0, 336.0), r=11.0, kick=SLING_KICK_L),
|
||||
dict(pos=(268.0, 336.0), r=11.0, kick=SLING_KICK_R),
|
||||
# guard posts above the outlane mouths: wall-huggers bounce back into
|
||||
# play - the centre gap is the real risk
|
||||
dict(pos=(52.0, 408.0), r=13.0, kick=(0.80, -0.60)),
|
||||
dict(pos=(308.0, 408.0), r=13.0, kick=(-0.80, -0.60)),
|
||||
# little floor-level posts that seal the outlane mouths
|
||||
dict(pos=(46.0, 432.0), r=9.0, kick=(0.72, -0.69)),
|
||||
dict(pos=(314.0, 432.0), r=9.0, kick=(-0.72, -0.69)),
|
||||
]
|
||||
STARS = [(168.0, 92.0), (240.0, 92.0), (312.0, 92.0)]
|
||||
STARS = [(120.0, 96.0), (180.0, 96.0), (240.0, 96.0)]
|
||||
|
||||
CANNON = (440.0, 152.0)
|
||||
CANNON = (300.0, 170.0)
|
||||
CANNON_R = 17.0
|
||||
LAUNCH_DIR = (-0.94, -0.34)
|
||||
LAUNCH_DIR = (-0.93, -0.36)
|
||||
LAUNCH_MIN, LAUNCH_MAX = 7.0, 13.5
|
||||
|
||||
DRAIN_Y = 362.0
|
||||
CENTER_HOLE = (200.0, 280.0)
|
||||
OUTLANE_L = 46.0
|
||||
OUTLANE_R = 434.0
|
||||
DRAIN_Y = 458.0
|
||||
CENTER_HOLE = (150.0, 210.0)
|
||||
OUTLANE_L = 44.0
|
||||
OUTLANE_R = 316.0
|
||||
|
||||
|
||||
def clamp(v, lo, hi):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Pixel-probe the rendered frames for layout sanity (since art can't be
|
||||
viewed directly in this session)."""
|
||||
viewed directly in this session). Portrait 360x480."""
|
||||
|
||||
import os
|
||||
|
||||
@@ -35,43 +35,45 @@ def main():
|
||||
st["lit"] = True
|
||||
g.table.ball_live = True
|
||||
g.pending = "live"
|
||||
g.table.ball.x, g.table.ball.y = 240, 300
|
||||
g.table.ball.x, g.table.ball.y = 180, 330
|
||||
g.table.ball.vx, g.table.ball.vy = 0, 3
|
||||
for _ in range(3):
|
||||
g.update()
|
||||
g.render()
|
||||
s = g.canvas
|
||||
checks = []
|
||||
# HUD white score digits around (14..100, 13..26)
|
||||
checks.append(("HUD score digits", count_px(s, (14, 13, 110, 15),
|
||||
lambda p: near(p, (255, 255, 255), 40)) > 25))
|
||||
# HUD gold BEST digits near (170..300, 13..26)
|
||||
checks.append(("HUD gold best", count_px(s, (170, 13, 150, 15),
|
||||
# HUD white score digits around (10..120, 13..26)
|
||||
checks.append(("HUD score digits", count_px(s, (10, 13, 110, 15),
|
||||
lambda p: near(p, (255, 255, 255), 40)) > 20))
|
||||
# HUD gold BEST digits near right (250..350, 13..26)
|
||||
checks.append(("HUD gold best", count_px(s, (230, 13, 120, 15),
|
||||
lambda p: near(p, C["gold"], 55)) > 10))
|
||||
# hearts red at (8..90, 40..50)
|
||||
checks.append(("hearts", count_px(s, (8, 40, 90, 12),
|
||||
lambda p: near(p, C["heart"], 50)) > 15))
|
||||
# three gold stars in HUD at (118..175, 36..48)
|
||||
checks.append(("HUD stars lit", count_px(s, (116, 36, 60, 14),
|
||||
lambda p: near(p, C["gold"], 55)) > 10))
|
||||
# hearts red at (336..390, 14..30)
|
||||
checks.append(("hearts", count_px(s, (336, 12, 60, 22),
|
||||
lambda p: near(p, C["heart"], 50)) > 20))
|
||||
# three gold stars in HUD at (52..112, 30..44)
|
||||
checks.append(("HUD stars lit", count_px(s, (44, 28, 90, 18),
|
||||
lambda p: near(p, C["gold"], 55)) > 15))
|
||||
# playfield interior present (not black)
|
||||
checks.append(("field fill", near(s.get_at((240, 100)), C["field"], 70)))
|
||||
# pink rail present on left edge inside play area
|
||||
rail_ok = count_px(s, (14, 70, 12, 20), lambda p: near(p, C["rail"], 60)) > 10
|
||||
checks.append(("field fill", near(s.get_at((180, 120)), C["field"], 70)))
|
||||
# left rail pink present inside play area
|
||||
rail_ok = count_px(s, (23, 100, 8, 20), lambda p: near(p, C["rail"], 60)) > 5
|
||||
checks.append(("left rail pink", rail_ok))
|
||||
# bumper pink at (150,180)
|
||||
checks.append(("bumper pink", near(s.get_at((150, 178)), C["pink"], 55)))
|
||||
# bumper lemon at (240,240)
|
||||
checks.append(("bumper lemon", near(s.get_at((240, 238)), C["lemon"], 55)))
|
||||
# pit dark center at (240, 352)
|
||||
checks.append(("pit center", near(s.get_at((240, 353)), C["pit"], 30)))
|
||||
# flipper blue present around (240, 320)
|
||||
checks.append(("flipper blue", count_px(s, (100, 300, 280, 45),
|
||||
lambda p: near(p, C["flipper"], 55)) > 30))
|
||||
# cannon purple body at (440,152)
|
||||
checks.append(("cannon", near(s.get_at((440, 152)), (150, 100, 220), 70)))
|
||||
# ball near (240,300) is white-ish
|
||||
checks.append(("ball", count_px(s, (230, 290, 20, 20),
|
||||
# bumper bodies (sample the rim, away from the face)
|
||||
checks.append(("bumper pink", near(s.get_at((82, 205)), C["pink"], 55)))
|
||||
checks.append(("bumper mint", count_px(s, (230, 190, 60, 40),
|
||||
lambda p: near(p, C["mint"], 55) or
|
||||
near(p, C["mint_l"], 55)) > 80))
|
||||
checks.append(("bumper lemon", near(s.get_at((160, 300)), C["lemon"], 55)))
|
||||
# pit dark center at (180, 455)
|
||||
checks.append(("pit center", near(s.get_at((180, 455)), C["pit"], 30)))
|
||||
# flippers present on BOTH sides (pivot ~78 and ~282)
|
||||
fl = count_px(s, (60, 395, 260, 50), lambda p: near(p, C["flipper"], 55))
|
||||
checks.append(("flipper blue both sides", fl > 60))
|
||||
# cannon purple body around (300,170)
|
||||
checks.append(("cannon", near(s.get_at((300, 170)), (150, 100, 220), 70)))
|
||||
# ball near (180,330) is white-ish
|
||||
checks.append(("ball", count_px(s, (170, 320, 20, 20),
|
||||
lambda p: near(p, (245, 250, 255), 40)) > 5))
|
||||
ok = True
|
||||
for name, passed in checks:
|
||||
|
||||
@@ -49,9 +49,9 @@ def main():
|
||||
t.reset_ball_in_cannon()
|
||||
g.pending = "live"
|
||||
t.ball_live = True
|
||||
t.ball.x, t.ball.y = 415, 60
|
||||
t.ball.x, t.ball.y = 295, 75
|
||||
t.ball.vx, t.ball.vy = -2.2, 3.1
|
||||
for _ in range(70):
|
||||
for _ in range(80):
|
||||
g.keys["fire"] = False
|
||||
g.update()
|
||||
save(g, "3_play", out)
|
||||
@@ -60,13 +60,13 @@ def main():
|
||||
g.combo = 4
|
||||
g.combo_t = 150
|
||||
g.fever = 60 * 12
|
||||
g.add_msg("+500", 150, 150, (255, 214, 92))
|
||||
g.add_msg("+500", 180, 150, (255, 214, 92))
|
||||
for st in t.stars:
|
||||
st["lit"] = True
|
||||
b = t.bumpers[0]
|
||||
b["squash"] = 4
|
||||
b["face_t"] = 0.0
|
||||
g.particles.append(dict(x=150, y=150, life=14, kind="ring", color=(255, 255, 255)))
|
||||
g.particles.append(dict(x=180, y=150, life=14, kind="ring", color=(255, 255, 255)))
|
||||
g.bubble = ["CANDY FEVER!", 90, "star"]
|
||||
save(g, "4_fever", out)
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |