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
This commit is contained in:
georgios
2026-09-09 19:02:11 +02:00
parent c08e22c873
commit cf2c0e4558
18 changed files with 134 additions and 128 deletions
+2 -2
View File
@@ -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.
![demo](shots/3_play.png)
+3 -3
View File
@@ -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
+62 -63
View File
@@ -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):
+32 -27
View File
@@ -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):
+31 -29
View File
@@ -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:
+4 -4
View File
@@ -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)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB