class Bonus(retro.Sprite): BONUS1 = types.SimpleNamespace( id=0, img=retro.Image(assets("bonus1.png")), color=(255, 184, 151), value=10, offset=(0, 0), ) BONUS2 = types.SimpleNamespace( id=1, img=retro.Image(assets("bonus2.png")), color=(255, 136, 84), value=50, offset=(-6, -6), ) def __new__(cls, pos, color): if color == cls.BONUS1.color: bonus = cls.BONUS1 elif color == cls.BONUS2.color: bonus = cls.BONUS2 else: return None self = retro.Sprite.__new__(cls) retro.Sprite.__init__(self, bonus.img) self.rect.topleft = numpy.add(pos, bonus.offset).tolist() self.id = bonus.id self.value = bonus.value return self def __init__(self, pos, color): pass
def TestInit(): s1 = retro.Image((100, 100)) s1_rect = s1.rect() s1_rect.move_ip(100, 10) s2 = retro.Image(assets("img.png")) s2_rect = s2.rect() s2_area = retro.Rect(20, 10, 30, 30) s2_rect.move_ip(100, 110) s3 = retro.Image(assets("trap.png")) s3_rect = s3.rect() s3_rect.move_ip(100, 150) s4 = s3.copy() s4_rect = s3_rect.copy() s4_rect.move_ip(50, 0) s5 = s4 s5_rect = s4_rect.copy() s5_rect.move_ip(50, 0) s5.draw_line(retro.GREEN, (0, 0), (30, 30)) def draw(target): target.draw_img(s1, s1_rect.topleft) target.draw_img(s2, s2_rect.topleft, s2_area) target.draw_img(s3, s3_rect.topleft) target.draw_img(s4, s4_rect.topleft) target.draw_img(s5, s5_rect.topleft) return draw
class Mine: ICON = retro.Image(asset("ui_mine.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.enabled = False return self def run(self): if not self.enabled: self.lemming.start_animation("MINE") self.enabled = True if not self.lemming.animations.finished: return self.lemming.bg.original.draw_circle( color=retro.BLACK, center=self.lemming.rect.center, radius=18, ) dx = self.lemming.actions.walk.dx self.lemming.rect.move_ip(3 * dx, 3) self.lemming.start_animation("MINE")
class Build: ICON = retro.Image(asset("ui_build.png")) def __init__(self, lemming): self.lemming = lemming self.count = 0 @property def finished(self): return (self.count >= 12) def start(self): self.lemming.start_animation("BUILD") return self def run(self): if not self.lemming.animations.finished: return dx = self.lemming.actions.walk.dx rect = self.lemming.rect.copy() rect.size = (15, 3) rect.top = self.lemming.rect.bottom - rect.height if (dx > 0): rect.left = self.lemming.rect.right - (rect.width // 2) elif (dx < 0): rect.left -= rect.width // 2 self.lemming.bg.original.draw_rect(retro.GREY, rect) self.lemming.rect.move_ip(dx * rect.width // 2, -rect.height) self.count += 1 self.lemming.start_animation("BUILD")
class Bird(retro.Sprite): IMG = retro.Image(assets("bird.png")) ACCEL = 1 DEFAULT_SPEED = -9 MAX_SPEED = 9 def __init__(self, window): retro.Sprite.__init__(self, self.IMG) self.window = window self.fitness = 0 self.alive = True self.rect.center = (75, self.window.rect().centery) self.flap() def flap(self): if self.alive: self.speed_y = self.DEFAULT_SPEED def collide(self, *elements): return any(self.rect.colliderect(e.rect) for e in elements) def update(self): if self.alive: self.speed_y += self.ACCEL self.speed_y = min(self.speed_y, self.MAX_SPEED) self.rect.y += self.speed_y self.fitness += 1 def draw(self): if self.alive: retro.Sprite.draw(self, self.window)
class Bomb: ICON = retro.Image(asset("ui_bomb.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.ticker = retro.Ticker(end=135) self.explode = False return self def run(self): if not self.explode: self.lemming.start_animation("BOMB") self.explode = True self.lemming.bg.original.draw_circle( color=retro.BLACK, center=self.lemming.bounding_rect.midbottom, radius=20, ) elif self.lemming.animations.finished: self.lemming.kill() def draw_ticker(self): if self.explode: return window = self.lemming.window ticker_surface = retro.Sprite(window.fonts[0].render( text=f"{self.ticker.remaining}", color=retro.WHITE, )) ticker_surface.rect.midbottom = self.lemming.bounding_rect.midtop ticker_surface.draw(window)
def TestTransform(): obj1 = retro.Image((50, 50)) obj1.draw_line( color=retro.GREEN, start_pos=(0, 0), end_pos=(25, 25), width=5, ) obj1_rect = obj1.rect() obj1_rect.move_ip(10, 300) obj2 = obj1.copy() obj2.flip(x=True, y=False) obj2_rect = obj2.rect() obj2_rect.topleft = obj1_rect.topright obj3 = retro.Image((50, 50)) obj3.fill(color=retro.BLUE) obj3.draw_rect( color=retro.WHITE, rect=retro.Rect(10, 10, 25, 25), width=4, ) obj3.rotate(45) obj3_rect = obj3.rect() obj3_rect.topleft = obj1_rect.bottomleft obj4 = obj3.copy() obj4.resize((25, 25)) obj4_rect = obj4.rect() obj4_rect.topleft = obj3_rect.topright obj5 = obj3.copy() obj5.scale(1.4) obj5_rect = obj5.rect() obj5_rect.topleft = obj3_rect.bottomleft def draw(target): target.draw_img(obj1, obj1_rect.topleft) target.draw_img(obj2, obj2_rect.topleft) target.draw_img(obj3, obj3_rect.topleft) target.draw_img(obj4, obj4_rect.topleft) target.draw_img(obj5, obj5_rect.topleft) return draw
class Explosion(retro.Sprite): IMG = retro.Image(asset("bang.png")) def __init__(self, center): retro.Sprite.__init__(self, self.IMG) self.rect.center = center self.ticker = retro.Ticker(end=120) def update(self): if self.ticker.finished: self.kill()
def __init__(self, window, nbirds): self.window = window self.nbirds = nbirds self.bg = retro.Image(assets("bg.png")) self.ground = Ground(window) self.pipes = Pipes(window) self.target = self.pipes[0] self.birds = [Bird(window) for i in range(nbirds)] self.finished = False
class Maze(retro.Sprite): IMG = retro.Image(assets('maze.png')) RANGEW = range(0, IMG.rect().w, 16) RANGEH = range(0, IMG.rect().h, 16) WALLS = None BONUSES = None # Defer constants initialization to avoid circular dependency. def __new__(cls): if not cls.WALLS: cls.WALLS = Walls() if not cls.BONUSES: cls.BONUSES = Bonuses() return retro.Sprite.__new__(cls) def __init__(self): retro.Sprite.__init__(self, self.IMG.copy()) self.bonuses = self.BONUSES.copy() self.walls = self.WALLS # Checks whether the (`i, `j`) coordinates are inside the maze. @classmethod def inside(cls, i, j): return (i in range(len(cls.RANGEW)) and j in range(len(cls.RANGEH))) @classmethod def tile_pos(cls, pos): return (pos[0] // 16, pos[1] // 16) # Returns a Maze iterator. # If `window` is specified, iterates over the neighborhood centered on # `window.center` and of reach `window.reach`. @classmethod def iterator(self, transpose=False, window=None): def windowit(range, i): minv = window.center[i] - window.reach maxv = window.center[i] + window.reach + 1 if minv < 0: maxv += abs(minv) minv = 0 if maxv >= len(range): minv -= maxv - len(range) maxv = len(range) return itertools.islice(enumerate(range), minv, maxv) it1, it2 = enumerate(self.RANGEW), enumerate(self.RANGEH) if window: it1, it2 = windowit(self.RANGEW, 0), windowit(self.RANGEH, 1) if transpose: it1, it2 = it2, it1 for ix, jy in itertools.product(it1, it2): if transpose: ix, jy = jy, ix yield ix, jy def draw(self, target): retro.Sprite.draw(self, target) self.bonuses.draw(target)
class Stop: ICON = retro.Image(asset("ui_stop.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.lemming.start_animation("STOP") return self def run(self): pass
class Crosshair(retro.Sprite): IMG = retro.Image(asset("crosshair.png")) def __init__(self, window): self.window = window retro.Sprite.__init__(self, self.IMG) self.rect.center = self.window.rect().center self.speed = 5 def scroll_vec(self): p = self.rect.center offset = 20 w = self.window.rect().w h = self.window.rect().h return retro.Directions( up=(0 <= p[1] <= offset), down=(h - offset <= p[1] <= h), left=(0 <= p[0] <= offset), right=(w - offset <= p[0] <= w), ).vec def move(self, stage): scroll_vec = self.scroll_vec() # move crosshair key_hold = self.window.events.key_hold move_vec = retro.Directions( up=key_hold(retro.K_UP), down=key_hold(retro.K_DOWN), left=key_hold(retro.K_LEFT), right=key_hold(retro.K_RIGHT), ).vec for i, _ in enumerate(move_vec): move_vec[i] -= scroll_vec[i] move_vec = numpy.clip(move_vec, -1, 1) self.rect.move_ip( move_vec[0] * self.speed, move_vec[1] * self.speed, ) # move camera stage.camera.move_ip( scroll_vec[0] * self.speed, scroll_vec[1] * self.speed, ) stage.camera.clamp_ip(stage.image.rect())
class Float: ICON = retro.Image(asset("ui_float.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.enabled = False return self def run(self): if not self.enabled: self.lemming.start_animation("FLOAT") self.enabled = True self.lemming.rect.move_ip(0, 1)
class Ground(retro.Sprite): IMG = retro.Image(assets("ground.png")) SPEED = 4 def __init__(self, window): retro.Sprite.__init__(self, self.IMG) self.window = window self.shift = (self.rect.width - self.window.rect().width) self.rect.bottom = self.window.rect().bottom def update(self): self.rect.x = (self.rect.x - self.SPEED) % -self.shift def draw(self): retro.Sprite.draw(self, self.window)
class Ammunitions(retro.Sprite): IMG = retro.Image(asset("bullet.png")) IMG.scale(0.5) def __init__(self, window): self.window = window retro.Sprite.__init__(self, self.IMG) self.rect.bottomleft = self.window.rect().bottomleft self.count = 12 def draw(self, dest): for i in range(self.count): self.rect.left = i * self.rect.width retro.Sprite.draw(self, dest)
class Kidnaper(Enemy): KIDNAPER_IMG = retro.Image(asset("bandit_kidnaper.png"), ) def __init__(self): Enemy.__init__(self, self.KIDNAPER_IMG) def kill(self, p): kidnaper = self.rect.copy() kidnaper.width //= 2 victim = self.rect.copy() victim.width //= 2 victim.x += victim.width victim_killed = (self.alive and victim.collidepoint(p)) kidnaper_killed = (self.alive and kidnaper.collidepoint(p)) if victim_killed or kidnaper_killed: self.alive = False return (kidnaper_killed - victim_killed)
class Hide(retro.Sprite): IMG = retro.Image(asset("hide.png")) def __init__(self, window): self.window = window retro.Sprite.__init__(self, self.IMG) self.rect.center = self.window.rect().center self.hidden = False def update(self): self.hidden = self.window.events.key_hold(retro.K_LSHIFT) def draw(self, dest): if self.hidden: retro.Sprite.draw(self, dest)
class Pipes(list): IMG_BOTTOM = retro.Image(assets("pipe.png")) IMG_TOP = IMG_BOTTOM.copy() IMG_TOP.flip(x=True, y=True) GAP_HEIGHT = 100 OFFSET_HEIGHT = 80 def __init__(self, window): self.window = window list.__init__(self) self.generate() self.generate() def generate(self): x = self.window.rect().right if not self else \ self[-1].left + (self.window.rect().right // 2) y = random.randrange( self.OFFSET_HEIGHT, self.window.rect().height - self.GAP_HEIGHT - Ground.IMG.rect().height - self.OFFSET_HEIGHT) ptop = retro.Sprite(self.IMG_TOP) ptop.rect.bottom = ptop.rect.top + y ptop.rect.left = x pbot = retro.Sprite(self.IMG_BOTTOM) pbot.rect.top = y + self.GAP_HEIGHT pbot.rect.left = x self.append(Pipe(ptop=ptop, pbot=pbot)) def update(self): for pipe in self: pipe.ptop.rect.x -= Ground.SPEED pipe.pbot.rect.x -= Ground.SPEED # generate new pipe when the leftmost pipe starts to disappear if (len(self) == 2) and (self[0].left < 0): self.generate() # delete leftmost pipe when it goes out of screen if self[0].right <= 0: del self[0] def draw(self): for pipe in self: pipe.ptop.draw(self.window) pipe.pbot.draw(self.window)
class Runner(Enemy): RUNNER_IMG = retro.Image(asset("bandit_street3.png"), ) def __init__(self, stage): Enemy.__init__(self, self.RUNNER_IMG) self.stage = stage self.dx = -2 def move(self): self.rect.x += self.dx if not self.stage.image.rect().contains(self.rect): self.dx *= -1 self.image.flip(x=True, y=False) self.rect.clamp_ip(self.stage.image.rect()) def update(self, target): Enemy.update(self, target) self.move()
class DigV: ICON = retro.Image(asset("ui_digv.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.lemming.start_animation("DIGV") return self def run(self): dx = self.lemming.actions.walk.dx rect = self.lemming.rect.copy() rect.top = rect.bottom - 1 rect.left += 10 if dx > 0 else 0 rect.size = (20, 2) self.lemming.bg.original.draw_rect(retro.BLACK, rect) self.lemming.rect.move_ip(0, 1)
class DigH: ICON = retro.Image(asset("ui_digh.png")) def __init__(self, lemming): self.lemming = lemming def start(self): self.enabled = False return self def run(self): if not self.enabled: self.lemming.start_animation("DIGH") self.enabled = True dx = self.lemming.actions.walk.dx rect = self.lemming.rect.copy() rect.left = rect.right - 15 if dx > 0 else rect.left - 1 rect.width = 16 self.lemming.bg.original.draw_rect(retro.BLACK, rect) self.lemming.rect.move_ip(dx, 0)
class Ghost(Entity): IMG = retro.Image(assets("ghost.png")) BONUS = 200 def __init__(self, pos): Entity.__init__( self, sprite=retro.Sprite( image=self.IMG, animations=retro.Animations( frame_size=(32, 32), period=3, WALK_L=([0], 0), WALK_U=([0], 0), WALK_R=([0], 0), WALK_D=([0], 0), FEAR_L=([1], 0), FEAR_U=([1], 0), FEAR_R=([1], 0), FEAR_D=([1], 0), ), ), pos=pos, speed=2, curdir=[0, 0], nxtdir=random.choice(([-1, 0], [1, 0])), ) self.state = State(self) @property def bounding_rect(self): return Entity.bounding_rect(self, 12) def next_dir(self): dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]] opdir = numpy.negative(self.curdir).tolist() if opdir in dirs: dirs.remove(opdir) self.nxtdir = random.choice(dirs) def collide_maze(self, maze): if not self.nxtcol: self.curdir = self.nxtdir elif self.curcol is None: self.curdir = numpy.negative(self.curdir).tolist() elif self.curcol: self.next_dir() return True return False def kill(self): for g in self.groups: g.notify_kill() Entity.kill(self) def update(self, maze, player): self.state.update(player) if (self.curdir == self.nxtdir): self.next_dir() Entity.update(self, maze)
class Bonuses(list): IMG = retro.Image(assets('bonuses.png')) def __init__(self): self.count = 0 list.__init__(self, []) for (i, x), (j, y) in Maze.iterator(): if j == 0: self.append([]) pos = (x + 6, y + 6) b = Bonus(pos, self.IMG[pos]) self[i].append(b) if b: self.count += 1 def copy(self): new = list.__new__(self.__class__) new.count = self.count list.__init__(new, [lst.copy() for lst in self]) return new def iterator(self): for i, line in enumerate(self): for j, b in enumerate(line): yield i, j, b # Search inside growing neighborhoods until a bonus is found. def nearest(self, sprite): max_reach = max(len(Maze.RANGEW), len(Maze.RANGEH)) for reach in range(0, max_reach): _, _, b = next(self.neighborhood(sprite, reach), (None, None, None)) if b: return b return None # Iterator yielding bonuses contained inside a neighborhood centered around # `sprite` and defined by a hollow rectangle of a specific `reach`. # examples: # reach = 0 | reach = 1 | reach = 2 # ····· | ····· | ▫▫▫▫▫ # ····· | ·▫▫▫· | ▫···▫ # ··▫·· | ·▫s▫· | ▫·s·▫ # ····· | ·▫▫▫· | ▫···▫ # ····· | ····· | ▫▫▫▫▫ def neighborhood(self, sprite, reach=0): i, j = Maze.tile_pos(sprite.rect.center) if reach == 0: it = ((0, 0), ) else: it = itertools.chain( itertools.product(range(-reach, reach + 1), (-reach, reach)), itertools.product((-reach, reach), range(-reach + 1, reach)), ) for k, l in it: k += i l += j if not Maze.inside(k, l): continue b = self[k][l] if b: yield k, l, b def remove(self, i, j): self[i][j] = None self.count -= 1 def draw(self, image): for _, _, b in self.iterator(): if b: image.draw_img(b.image, b.rect)
def __init__(self): self.tile_size = 20 retro.Sprite.__init__(self, retro.Image(asset('maze.png'))) self.image.scale(self.tile_size) self.init_items()
class Player(Entity): IMG = retro.Image(assets("pacman.png")) def __init__(self, pos): Entity.__init__( self, sprite=retro.Sprite( image=self.IMG, animations=retro.Animations( frame_size=(32, 32), period=3, STOP_L=([2], 0), STOP_U=([3], 0), STOP_R=([0], 0), STOP_D=([4], 0), WALK_L=([2, 1], 0), WALK_U=([3, 1], 0), WALK_R=([0, 1], 0), WALK_D=([4, 1], 0), ), ), pos=pos, speed=4, ) self.score = 0 self.powerup = Powerup() @property def bounding_rect(self): return Entity.bounding_rect(self, 4) def collide_maze(self, maze): if self.curcol is None: if self.curdir[0]: self.rect.centerx = abs(self.rect.centerx - maze.rect.w) if self.curdir[1]: self.rect.centery = abs(self.rect.centery - maze.rect.h) elif not self.nxtcol: self.set_animation("WALK") self.curdir = self.nxtdir elif self.curcol: self.set_animation("STOP") return True return False def collide_bonus(self, maze): for i, j, b in maze.bonuses.neighborhood(self): if not b.rect.colliderect(self.bounding_rect): continue maze.bonuses.remove(i, j) if (b.id == b.BONUS2.id): self.powerup.start() self.score += b.value # Returns 0 if no collision happened, # 1 if the player killed a ghost, # -1 if a ghost killed the player. def collide_ghost(self, ghosts): g = next((g for g in ghosts if g.bounding_rect.colliderect(self.bounding_rect)), None) if not g: return 0 if g.state != g.state.FEAR: return -1 g.kill() self.score += self.powerup.mul * g.BONUS self.powerup.mul *= 2 return 1 def update(self, maze): self.collide_bonus(maze) Entity.update(self, maze) def draw_score(self, image): font = retro.Font(36) txt = retro.Sprite( font.render( text=f"SCORE: {self.score}", color=retro.WHITE, bgcolor=retro.BLACK, )) txt.rect.bottomleft = image.rect().bottomleft txt.draw(image)
return draw # --- window = retro.Window( title='image', size=(800, 600), fps=30, headless='--interactive' not in sys.argv, ) test_draw = TestDraw() test_init = TestInit() test_tf = TestTransform() def render(target): target.fill(retro.WHITE) test_draw(target) test_init(target) test_tf(target) if window.headless: render(window) # window.save(assets('expectation_image.png')) assert window == retro.Image(assets('expectation_image.png')) else: window.loop(None, lambda: render(window))
class Lemming(retro.Sprite): IMG = retro.Image(asset("lemming.png")) def __init__(self, window, bg, position): self.window = window self.bg = bg revrange = lambda start, end: range(end - 1, start - 1, -1) retro.Sprite.__init__( self=self, image=self.IMG, animations=retro.Animations( frame_size=(30, 30), period=4, WALK_L=(range(0, 8), 0), WALK_R=(revrange(0, 8), 12), WALKA_L=(range(0, 8), 11), WALKA_R=(revrange(0, 8), 23), FALL_L=(range(0, 4), 1), FALL_R=(revrange(0, 4), 13), FLOAT_L=(range(0, 6), 3), FLOAT_R=(revrange(0, 6), 15), STOP_L=(range(0, 16), 4), STOP_R=(revrange(0, 16), 16), BOMB_L=(range(0, 14), 5), BOMB_R=(revrange(0, 14), 17), BUILD_L=(range(0, 16), 6), BUILD_R=(revrange(0, 16), 18), DIGV_L=(range(0, 16), 7), DIGV_R=(revrange(0, 16), 19), DIGH_L=(range(0, 12), 8), DIGH_R=(revrange(0, 12), 20), MINE_L=(range(0, 17), 9), MINE_R=(revrange(0, 17), 21), DEAD_L=(range(0, 16), 10), DEAD_R=(revrange(0, 16), 22), ), ) self.rect.topleft = position self.actions = Actions(self) self.state = None @property def bounding_rect(self): dx = self.actions.walk.dx rect = self.rect.copy() rect.width //= 2 if dx > 0: rect.left += rect.width return rect def set_animation(self, name): dx = self.actions.walk.dx if dx < 0: self.animations.set(f"{name}_L") elif dx > 0: self.animations.set(f"{name}_R") else: self.animations.set("NONE") def start_animation(self, name): dx = self.actions.walk.dx if dx < 0: self.animations.start(f"{name}_L") elif dx > 0: self.animations.start(f"{name}_R") else: self.animations.start("NONE") def collisions(self, surface): directions = retro.Collisions.pixel_mid(surface, self.bounding_rect, retro.BLACK).invert() outside = (None in directions) directions.replace(None, True) fall = not directions.down dx = self.actions.walk.dx side = (directions.vec[0] == dx) return types.SimpleNamespace( outside=outside, fall=fall, side=side, ) def update(self, new_action): collisions_all = self.collisions(self.bg.image) collisions_bg = self.collisions(self.bg.original) if self.state is None: self.state = self.actions.fall.start() elif self.state == self.actions.walk: if collisions_all.fall: self.state = self.actions.fall.start() elif new_action: self.state = self.actions.from_class(new_action).start() else: self.actions.walk.run(collisions_all) elif self.state == self.actions.fall: if collisions_all.fall: self.actions.fall.run() elif self.actions.fall.dead: self.state = self.actions.dead.start() else: self.actions.fall.clamp() self.state = self.actions.walk.start() elif self.state == self.actions.float: if collisions_all.fall: self.actions.float.run() elif self.actions.float.enabled: self.state = self.actions.walk.start() else: self.actions.walk.run(collisions_all, pending_action=True) elif self.state == self.actions.stop: self.actions.stop.run() elif self.state == self.actions.bomb: if self.actions.bomb.ticker.finished or collisions_all.fall: self.actions.bomb.run() else: self.actions.walk.run(collisions_all, pending_action=True) elif self.state == self.actions.build: if self.actions.build.finished or collisions_bg.side: self.state = self.actions.walk.start() else: self.actions.build.run() elif self.state == self.actions.digv: if (not collisions_bg.fall) and (not collisions_bg.outside): self.actions.digv.run() else: self.state = self.actions.walk.start() elif self.state == self.actions.digh: if (collisions_bg.side and (not collisions_bg.fall) and (not collisions_bg.outside)): self.actions.digh.run() elif collisions_all.fall or self.actions.digh.enabled: self.state = self.actions.walk.start() else: self.actions.walk.run(collisions_all, pending_action=True) elif self.state == self.actions.mine: if (collisions_bg.side and (not collisions_bg.fall) and (not collisions_bg.outside)): self.actions.mine.run() elif collisions_all.fall or self.actions.mine.enabled: self.state = self.actions.walk.start() else: self.actions.walk.run(collisions_all, pending_action=True) elif self.state == self.actions.dead: self.actions.dead.run() def draw_bg(self): if self.state == self.actions.stop: retro.Sprite.draw(self, self.bg.image) def draw_screen(self): if self.state == self.actions.stop: return elif self.state == self.actions.bomb: self.actions.bomb.draw_ticker() retro.Sprite.draw(self, self.window)