diff -ruN a/fujo/behaviors.py b/fujo/behaviors.py --- a/fujo/behaviors.py +++ b/fujo/behaviors.py @@ -36,7 +36,7 @@ @Behavior.register class Wander(Behavior): def act(self, level): - possible = [d for d in xrange(8) + possible = [d for d in range(8) if not level.is_blocked(dpos(self.mob.pos, d))] if possible: random.shuffle(possible) @@ -153,7 +153,7 @@ priority = 150 def act(self, level): - for d in xrange(8): + for d in range(8): if level.player.pos == dpos(self.mob.pos, d): self.mob.do_attack(level, d, level.player) return True @@ -223,7 +223,7 @@ self.angry -= 1 if self.angry <= 0: self.mob.del_behavior(self) - possible = [d for d in xrange(8) + possible = [d for d in range(8) if not level.is_blocked(dpos(self.mob.pos, d))] if possible: random.shuffle(possible) @@ -261,7 +261,7 @@ self.afraid -= 1 if self.afraid <= 0: self.mob.del_behavior(self) - possible = [d for d in xrange(8) + possible = [d for d in range(8) if not level.is_blocked(dpos(self.mob.pos, d))] if possible: random.shuffle(possible) @@ -318,7 +318,7 @@ self.last_possible = 8 def act(self, level): - possible = [d for d in xrange(8) + possible = [d for d in range(8) if not level.is_blocked(dpos(self.mob.pos, d))] if not possible: return False @@ -355,7 +355,7 @@ if self.mob.item: self.mob.del_behavior(self) return - for d in xrange(8): + for d in range(8): if level.player.pos == dpos(self.mob.pos, d): self.mob.do_steal(level, d, level.player) if self.mob.item: diff -ruN a/fujo/display.py b/fujo/display.py --- a/fujo/display.py +++ b/fujo/display.py @@ -1,6 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import asyncio +import time + import pygame from .resources import RES @@ -31,7 +34,7 @@ r = spritedict[s] newrect = surface_blit(s.image, s.rect, special_flags=pygame.BLEND_MULT) - if r is 0: + if r is None or r == 0: dirty_append(newrect) else: if newrect.colliderect(r): @@ -61,13 +64,16 @@ self.labels = Sprites() self.clock = pygame.time.Clock() self.quit = False + self._last = time.time() self.camera = pygame.Rect( 0, 0, - self.screen_size[0] / self.scale, - self.screen_size[1] / self.scale) + self.screen_size[0] // self.scale, + self.screen_size[1] // self.scale) - def tick(self): - self.clock.tick(24) + async def tick(self): + # 24 frames per second, waiting without keeping the browser busy + await asyncio.sleep(max(0.0, 1.0 / 24 - (time.time() - self._last))) + self._last = time.time() for event in pygame.event.get(): if event.type == pygame.QUIT: self.quit = True @@ -149,9 +155,9 @@ render_layer(self.background, level.walls, RES.wall_tiles[30 * level.looks + 30:], lambda c: c != 0, -8) - for mob in level.mobs.values(): + for mob in list(level.mobs.values()): self.spawn_mob(mob) - for thing in level.things.values(): + for thing in list(level.things.values()): self.spawn_thing(thing) self.update_explored(level) diff -ruN a/fujo/game.py b/fujo/game.py --- a/fujo/game.py +++ b/fujo/game.py @@ -58,13 +58,13 @@ camera.bottom = r.height - def tick(self): + async def tick(self): if pygame.key.get_pressed()[pygame.K_ESCAPE]: menu = window.Menu((256, 64, 80, 32), [ ('continue', 'Continue'), ('quit', 'Quit'), ]) - self.running = menu.run(self.display) in (None, 'continue') + self.running = (await menu.run(self.display)) in (None, 'continue') menu.kill() elif self.player.animation is None: margin = 48 @@ -85,7 +85,7 @@ if getattr(self.level.things.get(self.player.pos), 'is_exit', 0): self.player.animation = self.player.climb_anim() return - acted = self.player.act(self.level, self.display) + acted = await self.player.act(self.level, self.display) self.player.update(self.display) if acted: explored = self.level.explore(self.player.pos) @@ -93,13 +93,13 @@ self.player.explored += explored self.display.update_explored(self.level) self.display.refresh() - for mob in self.level.mobs.values(): + for mob in list(self.level.mobs.values()): mob.metabolize(self.level) - for mob in self.level.mobs.values(): + for mob in list(self.level.mobs.values()): if mob != self.player: mob.act(self.level) - def run(self, load_data=None): + async def run(self, load_data=None): self.running = True if load_data: next_level = level.Level.create(load_data) @@ -107,15 +107,15 @@ next_level = None self.new_level(next_level) while self.running: - self.tick() + await self.tick() self.display.update() - self.display.tick() + await self.display.tick() saves.delete() options = [ ('again', 'Play again'), ('exit', 'Exit to title'), ] menu = window.Menu((256, 64, 80, 32), options) - choice = menu.run(self.display) + choice = await menu.run(self.display) return choice == 'again' diff -ruN a/fujo/items.py b/fujo/items.py --- a/fujo/items.py +++ b/fujo/items.py @@ -21,7 +21,7 @@ if not self.animation: return try: - self.animation.next() + next(self.animation) except StopIteration: self.animation = None @@ -39,8 +39,8 @@ def throw_anim(self, pos, steps, direction, drop=False): self.rect.midbottom = 24 * pos[0], 16 * pos[1] - 3 - for step in xrange(steps): - for frame in xrange(2): + for step in range(steps): + for frame in range(2): self.rect.move_ip(DX[direction] * 12, DY[direction] * 8) self.depth = self.rect.bottom + 8 yield @@ -362,7 +362,7 @@ def throw(self, level, pos, mob): super(SharpenSpell, self).throw(level, pos, mob) - for mob in level.mobs.values(): + for mob in list(level.mobs.values()): if mob != level.player: mob.wound(0) self.__class__.identified = True diff -ruN a/fujo/level.py b/fujo/level.py --- a/fujo/level.py +++ b/fujo/level.py @@ -33,9 +33,9 @@ self.size = 21, 32 if player: self.depth = player.level - self.floor = [[0] * self.size[0] for i in xrange(self.size[1])] - self.walls = [[1] * self.size[0] for i in xrange(self.size[1])] - self.explored = [[0] * self.size[0] for i in xrange(self.size[1])] + self.floor = [[0] * self.size[0] for i in range(self.size[1])] + self.walls = [[1] * self.size[0] for i in range(self.size[1])] + self.explored = [[0] * self.size[0] for i in range(self.size[1])] self.things = {} self.mobs = {} self.rooms = [] @@ -45,10 +45,10 @@ rooms = [] r = random.randint for (x, y), (w, h) in AREAS: - width = r(1, w / 2) * 2 - height = r(1, h / 2) * 2 - x_offset = r(0, (w - width) / 2) * 2 - y_offset = r(0, (h - height) / 2) * 2 + width = r(1, w // 2) * 2 + height = r(1, h // 2) * 2 + x_offset = r(0, (w - width) // 2) * 2 + y_offset = r(0, (h - height) // 2) * 2 rooms.append(((x + x_offset, y + y_offset), (width, height))) random.shuffle(rooms) @@ -56,10 +56,10 @@ for room, next_room in zip(rooms, rooms[1:]): (x, y), (w, h) = room (nx, ny), (nw, nh) = next_room - x1 = nx + r(0, nw / 2) * 2 - y1 = ny + r(0, nh / 2) * 2 - x2 = x + r(0, w / 2) * 2 - y2 = y + r(0, h / 2) * 2 + x1 = nx + r(0, nw // 2) * 2 + y1 = ny + r(0, nh // 2) * 2 + x2 = x + r(0, w // 2) * 2 + y2 = y + r(0, h // 2) * 2 self._path((x1, y1), (x2, y2)) for (x, y), (w, h) in rooms: @@ -67,20 +67,22 @@ self.rooms = rooms - def _fill(self, (sx, sy), (width, height)): - for y in xrange(sy, sy + height): - for x in xrange(sx, sx + width): + def _fill(self, start, size): + (sx, sy), (width, height) = start, size + for y in range(sy, sy + height): + for x in range(sx, sx + width): self.walls[y][x] = 0 self.floor[y][x] = random.randrange(1, 5) - def _path(self, (sx, sy), (ex, ey)): - for x in xrange(sx, ex, 1 if sx < ex else -1): + def _path(self, start, end): + (sx, sy), (ex, ey) = start, end + for x in range(sx, ex, 1 if sx < ex else -1): if not self.walls[sy][x]: return self.walls[sy][x] = 0 if self.floor[sy][x] == 0: self.floor[sy][x] = 1 - for y in xrange(sy, ey, 1 if sy < ey else -1): + for y in range(sy, ey, 1 if sy < ey else -1): if not self.walls[y][ex]: return self.walls[y][ex] = 0 @@ -110,7 +112,7 @@ if attack > defense: damage = random.randint(1, attacker.damage + 1) defender.wound(damage) - for mob in self.mobs.values(): + for mob in list(self.mobs.values()): if mob != attacker: mob.mob_attacked(self, defender, damage) @@ -123,7 +125,7 @@ return item def pick_item(self, thing): - for mob in self.mobs.values(): + for mob in list(self.mobs.values()): mob.item_picked(self, thing) del self.things[thing.pos] thing.pick_up(self) @@ -160,8 +162,8 @@ def populate(self): self.empty = [] for (sx, sy), (w, h) in self.rooms: - for y in xrange(sy, sy + h + 1): - for x in xrange(sx, sx + w + 1): + for y in range(sy, sy + h + 1): + for x in range(sx, sx + w + 1): self.empty.append((x, y)) random.shuffle(self.empty) try: @@ -171,28 +173,28 @@ self.looks = 1 boss = mobs.Boss() self.place_mob(boss, self.empty.pop()) - for i in xrange(2): + for i in range(2): item = random.choice(items.FOOD)() self.place_thing(item, self.empty.pop()) - for i in xrange(4): + for i in range(4): item = random.choice(items.WEAPONS)() self.place_thing(item, self.empty.pop()) - for i in xrange(2): + for i in range(2): item = random.choice(items.ARMOR)() self.place_thing(item, self.empty.pop()) else: self.place_thing(things.Ladder(), self.empty.pop()) - for i in xrange(3 + 5 * self.depth): + for i in range(3 + 5 * self.depth): Monster = random.choice(monsters) self.place_mob(Monster(), self.empty.pop()) - for i in xrange(10 - 2 * self.depth): + for i in range(10 - 2 * self.depth): item = random.choice(items.ITEMS)() self.place_thing(item, self.empty.pop()) - for i in xrange(2): + for i in range(2): # Guaranteed food. item = random.choice(items.FOOD)() self.place_thing(item, self.empty.pop()) - for i in xrange(12): + for i in range(12): self.place_thing(random.choice(things.DEBRIS)(), self.empty.pop()) pos = self.empty.pop() self.place_thing(things.Hole(), pos) @@ -211,14 +213,14 @@ count = 0 for (rx, ry), (w, h) in self.rooms: if rx <= pos[0] <= rx + w and ry <= pos[1] <= ry + h: - for y in xrange(ry - 1, ry + h + 2): - for x in xrange(rx - 1, rx + w + 2): + for y in range(ry - 1, ry + h + 2): + for x in range(rx - 1, rx + w + 2): if self.explored[y][x] == 0: count += 1 self.explored[y][x] = 2 break - for y in xrange(max(0, pos[1] - 1), min(self.size[1], pos[1] + 2)): - for x in xrange(max(0, pos[0] - 1), min(self.size[1], pos[0] + 2)): + for y in range(max(0, pos[1] - 1), min(self.size[1], pos[1] + 2)): + for x in range(max(0, pos[0] - 1), min(self.size[1], pos[0] + 2)): if self.explored[y][x] == 0: self.explored[y][x] = 1 count += 1 @@ -237,8 +239,8 @@ 'looks': self.looks, 'rooms': self.rooms, 'empty': self.empty, - 'mobs': [mob.save() for mob in self.mobs.values()], - 'things': [thing.save() for thing in self.things.values()], + 'mobs': [mob.save() for mob in list(self.mobs.values())], + 'things': [thing.save() for thing in list(self.things.values())], 'identified': [s.__name__ for s in items.SPELLS if s.identified] }) return data diff -ruN a/fujo/mobs.py b/fujo/mobs.py --- a/fujo/mobs.py +++ b/fujo/mobs.py @@ -158,14 +158,14 @@ def update(self, display): super(Monster, self).update(display) try: - self.animation.next() + next(self.animation) except StopIteration: self.animation = self.walk_anim() - self.animation.next() + next(self.animation) def walk_anim(self, direction=None): - for frame in xrange(4): - for step in xrange(2): + for frame in range(4): + for step in range(2): if direction is not None: self.rect.move_ip(3 * DX[direction], 2 * DY[direction]) self.depth = self.rect.bottom @@ -176,11 +176,11 @@ self.image = self.frames[8 + self.looks] yield yield - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(3 * DX[direction], 2 * DY[direction]) self.depth = self.rect.bottom yield - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(-3 * DX[direction], -2 * DY[direction]) self.depth = self.rect.bottom yield @@ -194,7 +194,7 @@ def death_anim(self): size = 0 - for frame in xrange(8): + for frame in range(8): self.image = pygame.transform.scale( self.frames[8 + self.looks], (32, 32 + size * 4)).convert() size += frame diff -ruN a/fujo/player.py b/fujo/player.py --- a/fujo/player.py +++ b/fujo/player.py @@ -134,7 +134,7 @@ self.label(item.get_name(), delay=2, place=thing.rect.midtop) self.items.append(item) - def act(self, level, display): + async def act(self, level, display): if self.dead: return True keys = pygame.key.get_pressed() @@ -162,7 +162,7 @@ self.animation = self.walk_anim() return True elif keys[pygame.K_RETURN]: - return self.inventory(level, display) + return await self.inventory(level, display) return False def smoke(self, place, num=4): @@ -173,7 +173,7 @@ puff.rect.center = place self.new_sprites.append(puff) - def inventory(self, level, display): + async def inventory(self, level, display): menu = window.Menu((display.size[0] - 196, 4, 192, 32), [( item, @@ -182,14 +182,14 @@ (" (worn)" if item == self.armor else '') ) for item in self.items]) while True: - item = menu.run(display) + item = await menu.run(display) if item: submenu = window.Menu((display.size[0] - 200, 36, 96, 32), [ ('use', item.action), ('throw', "Throw"), ('drop', "Drop"), ], 2) - action = submenu.run(display) + action = await submenu.run(display) submenu.kill() if action: break @@ -240,15 +240,15 @@ self.new_sprites[:] = [] if self.animation: try: - self.animation.next() + next(self.animation) except StopIteration: self.animation = None else: self.image = self.frames[self.face] def walk_anim(self, direction=None): - for frame in xrange(4): - for step in xrange(2): + for frame in range(4): + for step in range(2): yield if direction is not None: self.rect.move_ip(3 * DX[direction], 2 * DY[direction]) @@ -257,11 +257,11 @@ def attack_anim(self, direction): self.image = self.frames[self.face + 1] - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(3 * DX[direction], 2 * DY[direction]) self.depth = self.rect.bottom + 1 yield - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(-3 * DX[direction], -2 * DY[direction]) self.depth = self.rect.bottom + 1 yield @@ -270,11 +270,11 @@ def throw_anim(self, direction, item): self.image = self.frames[self.face + 1] - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(3 * DX[direction], 2 * DY[direction]) self.depth = self.rect.bottom + 1 yield - for frame in xrange(3): + for frame in range(3): self.rect.move_ip(-3 * DX[direction], -2 * DY[direction]) self.depth = self.rect.bottom + 1 yield @@ -282,16 +282,16 @@ yield def die_anim(self): - for count in xrange(4): - for frame in xrange(8): + for count in range(4): + for frame in range(8): self.image = self.frames[frame] yield self.kill() def climb_anim(self): - for count in xrange(2): - for frame in xrange(4): - for step in xrange(2): + for count in range(2): + for frame in range(4): + for step in range(2): yield self.rect.move_ip(0, -1) self.image = self.frames[4 + frame * 8] diff -ruN a/fujo/resources.py b/fujo/resources.py --- a/fujo/resources.py +++ b/fujo/resources.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import StringIO +import io import pkgutil import tempfile import os @@ -11,18 +11,25 @@ +def data_path(filename): + # (files are read straight from the package folder: pkgutil.get_data and + # loading from file-like objects do not work under pygbag) + return os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + + def load_data(filename): - return pkgutil.get_data('fujo', filename) + with open(data_path(filename), 'rb') as f: + return f.read() def load_file(filename): - return StringIO.StringIO(load_data(filename)) + return io.BytesIO(load_data(filename)) def load_image(filename): - return pygame.image.load(load_file(filename), filename) + return pygame.image.load(data_path(filename)) def load_atlas(filename, width=32, height=None): @@ -31,33 +38,20 @@ w, h = width, height or width return [ image.subsurface((x * w, y * h, w, h)) - for y in xrange(0, image_height // h) - for x in xrange(0, image_width // w) + for y in range(0, image_height // h) + for x in range(0, image_width // w) ] def load_font(filename, size=8): - # For some reason we need to have a real file on Windows. - tmpdir = tempfile.mkdtemp() - fname = os.path.join(tmpdir, filename) - try: - with open(fname, 'wb') as f: - f.write(load_data(filename)) - font = pygame.font.Font(fname, size) - finally: - try: - os.remove(fname) - os.rmdir(tmpdir) - except: - pass - return font + return pygame.font.Font(data_path(filename), size) class Resources(object): def load(self): self.wall_tiles = load_atlas("walls.png", 24, 16) self.floor_tiles = load_atlas("floors.png", 24, 16) - self.font = load_font("pf_ronda_seven_bold.ttf") + self.font = load_font("Jersey15-Regular.ttf", 15) self.player = load_atlas("player.png", 32, 32) self.monsters = load_atlas("monsters.png", 32, 32) self.things = load_atlas("things.png", 32, 32) @@ -73,6 +67,6 @@ def load_music(self): filename = "music.ogg" - pygame.mixer.music.load(load_file(filename)) + pygame.mixer.music.load(data_path(filename)) RES = Resources() diff -ruN a/fujo/saves.py b/fujo/saves.py --- a/fujo/saves.py +++ b/fujo/saves.py @@ -3,10 +3,11 @@ import os -import simplejson +import json -SAVE_FILE = '~/.fujo.sav' +# On the web (pygbag) the page keeps the /saves folder in the browser +SAVE_FILE = '/saves/fujo.sav' if os.path.isdir('/saves') else '~/.fujo.sav' class Saveable(object): @@ -39,14 +40,14 @@ 'version': 3, 'level': level_data, } - with open(filename, 'wb') as f: - simplejson.dump(data, f, sort_keys=True, indent=' ') + with open(filename, 'w') as f: + json.dump(data, f, sort_keys=True, indent=' ') def load(): filename = os.path.expanduser(SAVE_FILE) - with open(filename, 'rb') as f: - data = simplejson.load(f) + with open(filename, 'r') as f: + data = json.load(f) if data['version'] != 3: raise ValueError('Bad save version %r' % data['version']) return data['level'] @@ -64,5 +65,5 @@ filename = os.path.expanduser(SAVE_FILE) try: os.remove(filename) - except IOError: + except OSError: pass diff -ruN a/fujo/things.py b/fujo/things.py --- a/fujo/things.py +++ b/fujo/things.py @@ -58,7 +58,7 @@ def update(self, display): try: - self.animation.next() + next(self.animation) except StopIteration: self.kill() diff -ruN a/fujo/title.py b/fujo/title.py --- a/fujo/title.py +++ b/fujo/title.py @@ -1,7 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import asyncio import random +import time import pygame @@ -34,9 +36,11 @@ self.windows = display.Sprites() self.clock = pygame.time.Clock() self.quit = False + self._last = time.time() - def tick(self): - self.clock.tick(20) + async def tick(self): + await asyncio.sleep(max(0.0, 1.0 / 20 - (time.time() - self._last))) + self._last = time.time() for event in pygame.event.get(): if event.type == pygame.QUIT: self.quit = True @@ -47,7 +51,7 @@ dirty = self.windows.draw(self.screen) pygame.display.update(dirty) - def run(self): + async def run(self): options = [ ('continue', 'Continue'), ('start', 'Start new game'), @@ -71,17 +75,17 @@ if menu.pos == 0: menu.pos = 1 menu.set_lines(label for choice, label in options) - choice = menu.run(self) + choice = await menu.run(self) if choice == 'start': run = True while run: - run = game.Game().run() + run = await game.Game().run() self.screen.blit(RES.title, (0, 0)) pygame.display.flip() elif choice == 'continue': - run = game.Game().run(saves.load()) + run = await game.Game().run(saves.load()) while run: - run = game.Game().run() + run = await game.Game().run() self.screen.blit(RES.title, (0, 0)) pygame.display.flip() elif choice == 'music off': diff -ruN a/fujo/util.py b/fujo/util.py --- a/fujo/util.py +++ b/fujo/util.py @@ -14,7 +14,8 @@ return pos[0] + DX[direction], pos[1] + DY[direction] -def dist((x1, y1), (x2, y2)): +def dist(pos1, pos2): + (x1, y1), (x2, y2) = pos1, pos2 return max(abs(x2 - x1), abs(y2 - y1)) diff -ruN a/fujo/window.py b/fujo/window.py --- a/fujo/window.py +++ b/fujo/window.py @@ -101,7 +101,7 @@ self.cursor.depth = self.depth + 1 self.pos = 0 - def depress(self, display): + async def depress(self, display): KEYS = [ pygame.K_UP, pygame.K_DOWN, @@ -111,18 +111,18 @@ keys = pygame.key.get_pressed() while any(keys[k] for k in KEYS): display.update() - display.tick() + await display.tick() keys = pygame.key.get_pressed() - def run(self, display): + async def run(self, display): display.windows.add(self) - self.depress(display) + await self.depress(display) display.windows.add(self.cursor) self.cursor.rect.top = self.rect.top + 10 + self.pos * 12 end = len(self.items) - 1 chosen = None while True: - display.tick() + await display.tick() if self.cursor.steps == 0: keys = pygame.key.get_pressed() if keys[pygame.K_UP]: @@ -133,7 +133,7 @@ self.cursor.rect.top = self.rect.top + 10 + end * 12 self.cursor.steps = 0 self.pos = end - self.depress(display) + await self.depress(display) elif keys[pygame.K_DOWN]: if self.pos < end: self.cursor.steps += 3 @@ -142,7 +142,7 @@ self.cursor.rect.top = self.rect.top + 10 self.cursor.steps = 0 self.pos = 0 - self.depress(display) + await self.depress(display) elif keys[pygame.K_ESCAPE] or display.quit: break elif keys[pygame.K_RETURN]: @@ -150,7 +150,7 @@ chosen = self.items[self.pos][0] break display.update() - self.depress(display) + await self.depress(display) self.cursor.kill() return chosen diff -ruN a/main.py b/main.py --- a/main.py +++ b/main.py @@ -0,0 +1,26 @@ +import asyncio + +# pygbag reads the imports of this file to install pygame-ce before the game's own modules load +import pygame + +from fujo import title +from fujo.resources import RES + +MODE = 480, 480 + + +async def main(): + pygame.display.init() + pygame.font.init() + try: + pygame.mixer.init() + except Exception: + pass + pygame.display.set_mode(MODE) + pygame.display.set_caption("Fujo") + RES.load() + while True: + # nothing to exit to on a web page: "Exit" brings the title screen back + await title.Title().run() + +asyncio.run(main())