diff -ruN a/main.py b/main.py --- a/main.py 1969-12-31 21:00:00.000000000 -0300 +++ b/main.py 2026-11-04 23:39:22.280066112 -0300 @@ -0,0 +1,6 @@ +import pygame +import asyncio + +from src import main + +asyncio.run(main.main()) diff -ruN a/run_game.py b/run_game.py --- a/run_game.py 2026-11-04 23:35:32.091530257 -0300 +++ b/run_game.py 1969-12-31 21:00:00.000000000 -0300 @@ -1,5 +0,0 @@ -#! /usr/bin/env python - -from src import main -main.main() - diff -ruN a/src/data.py b/src/data.py --- a/src/data.py 2026-11-04 23:35:32.148931566 -0300 +++ b/src/data.py 2026-11-04 23:39:22.317116592 -0300 @@ -17,9 +17,7 @@ if flip: i = pygame.transform.flip(i, True, False) if cfilter: - pix = pygame.surfarray.pixels3d(i) - for c in (0,1,2): - pix[:,:,c] = pix[:,:,c] * (cfilter[c] / 400.) + i.fill((int(255 * cfilter[0] // 400), int(255 * cfilter[1] // 400), int(255 * cfilter[2] // 400), 255), special_flags=pygame.BLEND_RGB_MULT) imgcache[key] = i return imgcache[key] @@ -49,7 +47,10 @@ if not queue and speechchannel.get_busy(): return if sname not in soundcache: - soundcache[sname] = pygame.mixer.Sound(filename(sname + ".ogg")) + try: + soundcache[sname] = pygame.mixer.Sound(filename(sname + ".ogg")) + except (FileNotFoundError, pygame.error): + return # the speech recordings are not part of the published game sound = soundcache[sname] speechchannel.queue(sound) diff -ruN a/src/effect.py b/src/effect.py --- a/src/effect.py 2026-11-04 23:35:32.148378654 -0300 +++ b/src/effect.py 2026-11-04 23:39:22.053866722 -0300 @@ -1,5 +1,5 @@ import pygame -import vista, gamestate, data, settings +from . import vista, gamestate, data, settings def bordertext(text, fontname, fontsize, color0, color1=(255,255,255), d=1, cache={}): key = text, fontname, fontsize, color0, color1 @@ -38,13 +38,15 @@ area = 0, 0, int(rect.width * s), rect.height vista.screen.blit(self.surf, rect, area) - def __nonzero__(self): + def __bool__(self): return self.t < 4 class Beam(object): lifetime = 0.25 layer = 0 - def __init__(self, (x0, y0, z0), (x1, y1, z1), color): + def __init__(self, xxx_todo_changeme, xxx_todo_changeme1, color): + (x0, y0, z0) = xxx_todo_changeme + (x1, y1, z1) = xxx_todo_changeme1 self.x = (x0 + x1) / 2 self.y = (y0 + y1) / 2 self.x0, self.y0, self.z0 = x0, y0, z0 @@ -67,7 +69,8 @@ lifetime = 1. vz = 40 layer = 0 - def __init__(self, (x, y)): + def __init__(self, xxx_todo_changeme2): + (x, y) = xxx_todo_changeme2 self.x, self.y = x, y self.t = 0 @@ -88,7 +91,8 @@ class Bomb(object): lifetime = 0.5 layer = -1 - def __init__(self, (x, y)): + def __init__(self, xxx_todo_changeme3): + (x, y) = xxx_todo_changeme3 self.x, self.y = x, y self.t = 0 diff -ruN a/src/foe.py b/src/foe.py --- a/src/foe.py 2026-11-04 23:35:32.146009299 -0300 +++ b/src/foe.py 2026-11-04 23:39:22.074880462 -0300 @@ -1,7 +1,7 @@ # Who's the REAL monster, ya'll? import pygame, math, random -import vista, gamestate, data, effect, settings +from . import vista, gamestate, data, effect, settings class Foe(object): v = 2.0 diff -ruN a/src/gamemap.py b/src/gamemap.py --- a/src/gamemap.py 2026-11-04 23:35:32.147023624 -0300 +++ b/src/gamemap.py 2026-11-04 23:39:22.315655313 -0300 @@ -1,7 +1,7 @@ # The main game map drawing routine -import pygame, random, math, numpy -import vista, gamestate, settings, mechanics +import pygame, random, math +from . import vista, gamestate, settings, mechanics mode = None # current button mode @@ -9,8 +9,8 @@ def mapimg(zoomx, zoomy, cache = {}): key = zoomx, zoomy, gamestate.level if key in cache: return cache[key] - zoomx /= 4 - zoomy /= 2 + zoomx //= 4 + zoomy //= 2 size = gamestate.mapx * zoomx, gamestate.mapy * zoomy gx, gy = 3 * zoomx, 3 * zoomy @@ -20,11 +20,13 @@ for y in range(gy): dx, dy = (x - gx/2.) / gx * 3, (y - gy/2.) / gy * 3 a = math.exp(-(dx ** 2 + dy ** 2)) - gaussian.set_at((x, y), (255, 255, 255, 255*a)) + gaussian.set_at((x, y), (255, 255, 255, int(255*a))) bgaussian = pygame.Surface((gx, gy)).convert_alpha() bgaussian.fill((0,0,0,0)) - pygame.surfarray.pixels_alpha(bgaussian)[:,:] = pygame.surfarray.pixels_alpha(gaussian) + for x in range(gx): + for y in range(gy): + bgaussian.set_at((x, y), (0, 0, 0, gaussian.get_at((x, y))[3])) yellow = 255, 255, 0 black = 0, 0, 0 @@ -40,13 +42,10 @@ p0 = int((x - 1) * zoomx), int((y - 1) * zoomy) c = gamestate.map0.get_at((x, y)) layer.blit((gaussian if c in colors else bgaussian), p0) - alpha = pygame.surfarray.array3d(layer)[:,:,0] - alpha = numpy.maximum(numpy.minimum(alpha * 1.8 - 120 - (numpy.random.rand(*size) * 80), 255), 0) - pygame.surfarray.pixels_alpha(layer)[:,:] = alpha - pix = pygame.surfarray.pixels3d(layer) - pix[:,:,0] = numpy.random.rand(*size) * dr + r0 - pix[:,:,1] = numpy.random.rand(*size) * dg + g0 - pix[:,:,2] = numpy.random.rand(*size) * db + b0 + for x in range(size[0]): + for y in range(size[1]): + a = layer.get_at((x, y))[0] * 1.8 - 120 - random.random() * 80 + layer.set_at((x, y), (int(random.random() * dr + r0), int(random.random() * dg + g0), int(random.random() * db + b0), int(max(min(a, 255), 0)))) return layer road = makelayer((yellow,), 80, 20, 80, 20, 10, 0) @@ -55,11 +54,9 @@ img = pygame.transform.smoothscale(gamestate.map0, size) - cpix = pygame.surfarray.pixels3d(img) - cpix[:,:,0] = numpy.random.rand(*size) * 10 - cpix[:,:,1] = numpy.random.rand(*size) * 15 + 70 - cpix[:,:,2] = numpy.random.rand(*size) * 10 - del cpix + for x in range(size[0]): + for y in range(size[1]): + img.set_at((x, y), (int(random.random() * 10), int(random.random() * 15 + 70), int(random.random() * 10))) img.blit(water, (0, 0)) img.blit(grave, (0, 0)) img.blit(road, (0, 0)) diff -ruN a/src/gamescene.py b/src/gamescene.py --- a/src/gamescene.py 2026-11-04 23:35:32.149289480 -0300 +++ b/src/gamescene.py 2026-11-04 23:39:22.273386043 -0300 @@ -1,17 +1,17 @@ import pygame, random -import vista, gamemap, tech, gamestate, foe, data, scene, effect, settings, mechanics +from . import vista, gamemap, tech, gamestate, foe, data, scene, effect, settings, mechanics class GameScene(object): def __init__(self): gamestate.loadlevel() data.playspeech("level0", queue=True) - data.playmusic(["wander", "chill", "drive", "wander", "lame-monster-party"][gamestate.level]) + data.playmusic(["wander", "chill", "drive", "wander", "drive"][gamestate.level]) self.title = effect.Title(mechanics.titles[gamestate.level]) self.wintitle = effect.Title("Victory!") self.losetitle = effect.Title("Defeat!" if gamestate.level < 4 else "The end. Thank you!") self.won = False self.t = 0 - self.foequeue = sorted(mechanics.foequeues[gamestate.level]) + self.foequeue = sorted(mechanics.foequeues[gamestate.level], key=lambda e: e[0]) def think(self, dt, events): if settings.doubletime: @@ -70,7 +70,7 @@ if self.won: self.wintitle.think(dt) if not self.wintitle: - gamestate.level += 1 + gamestate.level = min(gamestate.level + 1, 4) # there is no level after the last one scene.pop() scene.push(GameScene()) elif gamestate.hp > 0: diff -ruN a/src/gamestate.py b/src/gamestate.py --- a/src/gamestate.py 2026-11-04 23:35:32.143856276 -0300 +++ b/src/gamestate.py 2026-11-04 23:39:22.125239747 -0300 @@ -1,5 +1,5 @@ import pygame -import mechanics, data, tower, settings, vista, effect, gamemap, tech +from . import mechanics, data, tower, settings, vista, effect, gamemap, tech level = 0 @@ -19,7 +19,7 @@ if key in rbindings and rbindings[key] in bindings: del bindings[rbindings[key]] bindings[objname] = key - rbindings = dict((o,k) for k,o in bindings.items()) + rbindings = dict((o,k) for k,o in list(bindings.items())) def damage(dhp): global hp @@ -29,7 +29,7 @@ global buildmasks, homex, homey buildmasks = dict((invention, pygame.Surface((mapx, mapy)).convert_alpha()) for invention in mechanics.inventions) - for mask in buildmasks.values(): + for mask in list(buildmasks.values()): mask.fill((255, 0, 0, 100)) for x in range(mapx): for y in range(mapy): @@ -58,7 +58,8 @@ def cansort(): return sortcost() <= bank -def canbuild(tech, (x, y)): +def canbuild(tech, xxx_todo_changeme): + (x, y) = xxx_todo_changeme c = cost(tech) if c is None or c > bank: return False @@ -73,7 +74,8 @@ return False return True -def build(tech, (x, y)): +def build(tech, xxx_todo_changeme1): + (x, y) = xxx_todo_changeme1 global bank, sortmode if not canbuild(tech, (x, y)): return @@ -84,7 +86,7 @@ mask = buildmasks[invention] for dx, dy in footprint: px, py = x+dx, y+dy - for mask in buildmasks.values(): + for mask in list(buildmasks.values()): mask.set_at((px, py), (255, 0, 0, 100)) sortmode = False return True diff -ruN a/src/main.py b/src/main.py --- a/src/main.py 2026-11-04 23:35:32.147537161 -0300 +++ b/src/main.py 2026-11-04 23:39:22.273066296 -0300 @@ -1,15 +1,21 @@ +import asyncio +import time import pygame -import scene, vista, settings, gamescene, gamestate, data +from . import scene, vista, settings, gamescene, gamestate, data -def main(): +async def main(): pygame.mixer.pre_init(11000, -16, 1, 256) pygame.init() data.initsound() vista.init() scene.push(gamescene.GameScene()) - clock = pygame.time.Clock() + last = time.time() while True: - dt = min(0.001 * clock.tick(settings.maxfps), 1. / settings.minfps) + # frame limiter for the browser: the page needs control back every frame + await asyncio.sleep(max(0.0, 1.0 / settings.maxfps - (time.time() - last))) + now = time.time() + dt = min(now - last, 1. / settings.minfps) + last = now if settings.showfps: pygame.display.set_caption("%.1fps" % clock.get_fps()) s = scene.top() @@ -22,9 +28,6 @@ if event.type == pygame.QUIT: gamestate.save() return - if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE: - gamestate.save() - return if event.type == pygame.KEYDOWN and event.key == pygame.K_F12: pygame.image.save(vista.screen, "screenshot.png") diff -ruN a/src/mechanics.py b/src/mechanics.py --- a/src/mechanics.py 2026-11-04 23:35:32.145173015 -0300 +++ b/src/mechanics.py 2026-11-04 23:39:22.312048301 -0300 @@ -1,5 +1,5 @@ import pygame -import settings, foe +from . import settings, foe elements = ["laser", "freeze", "fire", "plague", "atomic"] inventions = ["spire", "monkey", "shark", "corpse", "glyph"] @@ -83,8 +83,8 @@ homecolor = 255, 255, 255 def parsepath(s): - c = map(int, s.split()) - return zip(c[0::2], c[1::2]) + c = list(map(int, s.split())) + return list(zip(c[0::2], c[1::2])) paths = [ @@ -119,12 +119,12 @@ foequeues[1] += [(t+ 1.5, foe.Dog, paths[1][0]) for t in range(70, 160, 3)] foequeues[2] = [(t, foe.Villager, paths[2][t%2]) for t in range(10, 160, 3)] -foequeues[2] += [(t+ 1.5, foe.Villager, paths[2][t/2%2]) for t in range(70, 160, 2)] +foequeues[2] += [(t+ 1.5, foe.Villager, paths[2][t//2%2]) for t in range(70, 160, 2)] foequeues[2] += [(t+ 2.5, foe.Dog, paths[2][t%2]) for t in range(70, 160, 3)] foequeues[2] += [(t+ 3.5, foe.Soldier, paths[2][t%2]) for t in range(10, 160, 5)] foequeues[3] = [(t, foe.Villager, paths[3][t%2]) for t in range(10, 160, 3)] -foequeues[3] += [(t+ 1.5, foe.Villager, paths[3][t/2%2]) for t in range(70, 160, 2)] +foequeues[3] += [(t+ 1.5, foe.Villager, paths[3][t//2%2]) for t in range(70, 160, 2)] foequeues[3] += [(t+ 2.5, foe.Dog, paths[3][t%2]) for t in range(70, 160, 3)] foequeues[3] += [(t+ 3.5, foe.Soldier, paths[3][t%2]) for t in range(10, 160, 5)] foequeues[3] += [(t+ 3.5, foe.Horseman, paths[3][t%2]) for t in range(13, 160, 5)] diff -ruN a/src/settings.py b/src/settings.py --- a/src/settings.py 2026-11-04 23:35:32.146619252 -0300 +++ b/src/settings.py 2026-11-04 23:39:22.314701654 -0300 @@ -2,7 +2,7 @@ import sys -import data +from . import data gamename = "Uslingborg Defense" showfps = "--showfps" in sys.argv @@ -11,7 +11,7 @@ wsize = wx, wy = 24*26, 12*40 # gameplay window size msize = mx, my = wx//3, wy//3 # mini window size wx0, wy0 = wx//2, wy//2 # main gameplay window center -mx0, my0 = sx-mx/2, my/2 # mini window position +mx0, my0 = sx-mx//2, my//2 # mini window position doubleview = False # Need to fix input if this is going to work animals = "--noanimals" not in sys.argv # whether animalS appear (and can be killed) in the game diff -ruN a/src/tech.py b/src/tech.py --- a/src/tech.py 2026-11-04 23:35:32.142609994 -0300 +++ b/src/tech.py 2026-11-04 23:39:22.308706204 -0300 @@ -1,7 +1,7 @@ # the tech tree import pygame -import vista, gamestate, data, settings, mechanics, effect +from . import vista, gamestate, data, settings, mechanics, effect drag = None dragpos = None @@ -86,7 +86,7 @@ dragz = min(max(int((dragpos - tabley0) / w + 0.5), 0), len(gamestate.elements) - 1) elif drag in gamestate.inventions: dragpos = event.pos[0] - dragz = min(max(int((dragpos - tablex0) / w + 0.5), 0), len(gamestate.inventions) - 1) + dragz = min(max(int((dragpos - tablex0) // w + 0.5), 0), len(gamestate.inventions) - 1) if drag: if event.type == pygame.MOUSEBUTTONUP and event.button == 1: if drag in gamestate.elements: diff -ruN a/src/tower.py b/src/tower.py --- a/src/tower.py 2026-11-04 23:35:32.145661885 -0300 +++ b/src/tower.py 2026-11-04 23:39:22.217322348 -0300 @@ -1,12 +1,13 @@ import pygame, math -import vista, foe, gamestate, mechanics, data, effect +from . import vista, foe, gamestate, mechanics, data, effect class Tower(object): color = 50, 125, 200 anchor = "center" layer = 0 - def __init__(self, tech, (x, y)): + def __init__(self, tech, xxx_todo_changeme): + (x, y) = xxx_todo_changeme self.element, self.invention = tech.split() dx, dy = mechanics.footoffs[self.invention] self.x, self.y = x + dx, y + dy @@ -92,7 +93,8 @@ if target: self.attack(target) - def drawglow(self, (px, py)): + def drawglow(self, xxx_todo_changeme1): + (px, py) = xxx_todo_changeme1 if self.chargetimer < 0.5: pass elif self.chargetimer < self.chargetime: @@ -149,7 +151,8 @@ class Castle(object): layer = 0 - def __init__(self, (x, y)): + def __init__(self, xxx_todo_changeme2): + (x, y) = xxx_todo_changeme2 self.x, self.y = x + 0.5, y + 0.5 self.img = pygame.transform.smoothscale(data.img("castle"), (120, 248)) self.alive = True diff -ruN a/src/vista.py b/src/vista.py --- a/src/vista.py 2026-11-04 23:35:32.143077574 -0300 +++ b/src/vista.py 2026-11-04 23:39:22.309473156 -0300 @@ -1,5 +1,5 @@ import pygame -import settings, gamestate +from . import settings, gamestate def init(): global screen, mapwindow, techwindow, minimapwindow, _screen @@ -36,7 +36,7 @@ if ftrans: return mode = not mode ftrans = settings.transtime - import gamemap + from . import gamemap gamemap.mode = None def think(dt): @@ -61,15 +61,17 @@ mapx, mapy, mapsx, mapsy = mx, my, msx, msy -def worldpos((x, y)): +def worldpos(xxx_todo_changeme): + (x, y) = xxx_todo_changeme if settings.doubleview: x, y = x//2, y//2 - return vx0 + int((x - settings.wx0) / zoomx), vy0 + int((y - settings.wy0) / zoomy) + return vx0 + int((x - settings.wx0) // zoomx), vy0 + int((y - settings.wy0) // zoomy) # coordinates within the map window -def mappos((x, y)): - px = int(settings.wx/2 - zoomx * (gamestate.mapx/2 - x)) - py = int(settings.wy/2 - zoomy * (gamestate.mapy/2 - y)) +def mappos(xxx_todo_changeme1): + (x, y) = xxx_todo_changeme1 + px = int(settings.wx//2 - zoomx * (gamestate.mapx//2 - x)) + py = int(settings.wy//2 - zoomy * (gamestate.mapy//2 - y)) return px, py def clear(color = (0, 0, 0)):