diff --git a/main.py b/main.py index bfc5459..d13f02d 100644 --- a/main.py +++ b/main.py @@ -13,13 +13,14 @@ class Main: Main object for the game, though most of the interface is operated by the src-stored objects. """ + situation = None def __init__(self): pyxel.load("resource.pyxres") self.situation = init_class(stages_list["menu"], 0) pyxel.run(self.update, self.draw) - + def update(self): self.situation.update() # If the situation "ends", jump into the next one @@ -29,8 +30,8 @@ def update(self): if self.situation.nextlevel not in ("menu", "death"): write_savedata({"level": self.situation.nextlevel}) self.situation = init_class(stages_list[self.situation.nextlevel], tmp) - del(tmp) # we have to remove 'tmp' ASAP - + del tmp # we have to remove 'tmp' ASAP + def draw(self): self.situation.draw() draw_stats( @@ -38,7 +39,7 @@ def draw(self): self.situation.draw_v, self.situation.player_choice, self.situation.get_coin_count(), - str(type(self.situation)) + str(type(self.situation)), ) diff --git a/noxfile.py b/noxfile.py index 62fc976..d0e8242 100644 --- a/noxfile.py +++ b/noxfile.py @@ -9,15 +9,18 @@ "src/characters.py", "src/levels.py", "src/menu.py", - "src/tools.py" + "src/tools.py", ) + @nox.session def format(session: nox.Session): "Format the codebase." session.install("-r", "requirements.txt") session.install("-r", "test-requirements.txt") session.run("ruff", "check", *files, "--fix") # TODO: ignore certain rules? + session.run("ruff", "format", *files) # a reinforcement to 'ruff check --fix' + @nox.session def lint(session: nox.Session): @@ -26,6 +29,7 @@ def lint(session: nox.Session): session.install("-r", "test-requirements.txt") session.run("ruff", "check", *files) # TODO: ignore certain rules? + @nox.session(name="reset-savedata") def reset_savedata(session: nox.Session): "Clean up 'savedata.json', which should not have any contents, use it carefully." @@ -33,7 +37,5 @@ def reset_savedata(session: nox.Session): session.run( "python", "-c", - "import io; js = io.open('savedata.json', 'w'); " - f"js.write('{new_data}'); " - "js.close()" + "import io; js = io.open('savedata.json', 'w'); " f"js.write('{new_data}'); " "js.close()", ) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..d850617 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,4 @@ +line-length = 120 + +[lint] +extend-select = ["E501"] \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py index 5b3db6d..e7b42c0 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,7 +2,7 @@ from . import menu, levels, scenes -__all__ = ("stages_list") +__all__ = "stages_list" # Below there's a dictionary with all the objects for further use stages_list = { diff --git a/src/characters.py b/src/characters.py index 3a47bc2..bf0e222 100644 --- a/src/characters.py +++ b/src/characters.py @@ -7,7 +7,7 @@ # Some of the functions/protocols were borrowed from # another project of mine, "Abandon the ship!", which # is based in Pyxel example #10, "Platformer". -# +# # To be honest, "Diddi and Eli" can be considered a spiritual # successor to "Abandon the ship!"... @@ -33,7 +33,7 @@ "Slimehorn4", "Bullet", "Coin", - "BaseLevel" + "BaseLevel", ) SCROLL_BORDER_X = 80 @@ -61,13 +61,16 @@ scroll_x = 0 TOTAL_COINS = 0 + def adjust_x(real_x): return scroll_x + real_x + def get_tile(tile_x, tile_y): # print(tile_x, tile_y) return pyxel.tilemaps[1].pget(tile_x, tile_y) + def detect_collision(x, y, dy): x1 = x // 8 y1 = y // 8 @@ -83,10 +86,12 @@ def detect_collision(x, y, dy): return True return False + def is_wall(x, y): tile = get_tile(x // 8, y // 8) return tile in TILES_FLOOR or tile[0] >= WALL_TILE_X + def push_back(x, y, dx, dy): # TODO: We have to fix this function to make it work on # level 2 and above, there's currently a bug with that. @@ -117,6 +122,7 @@ def push_back(x, y, dx, dy): x += sign return x, y, dx, dy + # === Players === @@ -124,6 +130,7 @@ class Player1: """ Diddi, Player 1, operated using WASD keys. """ + alive = True already_jumping = False @@ -155,11 +162,11 @@ def initial_setup(self): self.key_bullet = pyxel.KEY_S self.key_right = pyxel.KEY_D self.imagebank = [ - (8, 0), # Right, normal + (8, 0), # Right, normal (16, 0), # Right, walking (1) (24, 0), # Right, walikng (2) (32, 0), # Right, jumping - (8, 8), # Left, normal + (8, 8), # Left, normal (16, 8), # Left, walking (1) (24, 8), # Left, walikng (2) (32, 8), # Left, jumping @@ -212,10 +219,10 @@ def update(self): if pyxel.btnp(self.key_bullet): if self.r_facing: # Send a bullet to the right - self.bullets.append(Bullet(self.x+6, self.y+3)) + self.bullets.append(Bullet(self.x + 6, self.y + 3)) else: # Send a bullet to the left - self.bullets.append(Bullet(self.x, self.y+3, False)) + self.bullets.append(Bullet(self.x, self.y + 3, False)) if pyxel.btn(self.key_left): # Move to the left self.dx = -2 @@ -257,7 +264,7 @@ def draw(self): class Player2(Player1): """ Eli, Player 2, operated with arrow keys. - + NOTE: this class is inherited from Diddi (Player1) as it uses most of its structure. However, some variables have changed (see @@ -274,14 +281,14 @@ def initial_setup(self): self.key_bullet = pyxel.KEY_DOWN self.key_right = pyxel.KEY_RIGHT self.imagebank = [ - (8, 16), # Right, normal - (16, 16), # Right, walking (1) - (24, 16), # Right, walikng (2) - (32, 16), # Right, jumping - (8, 24), # Left, normal - (16, 24), # Left, walking (1) - (24, 24), # Left, walikng (2) - (32, 24), # Left, jumping + (8, 16), # Right, normal + (16, 16), # Right, walking (1) + (24, 16), # Right, walikng (2) + (32, 16), # Right, jumping + (8, 24), # Left, normal + (16, 24), # Left, walking (1) + (24, 24), # Left, walikng (2) + (32, 24), # Left, jumping ] self.icon = (0, 24) @@ -291,6 +298,7 @@ def initial_setup(self): class BaseMob: "Simple base for all the mobs." + alive = False def __init__(self, x, y, yzero=0): @@ -307,12 +315,14 @@ def update(self): def draw(self): if self.alive: self.draw_template() - + def draw_template(self): pass + class Onion(BaseMob): "Mobs that just walk but can fall from cliffs." + direction = -1 def update(self): @@ -331,20 +341,18 @@ def draw_template(self): v = random.choice([48, 56]) pyxel.blt(self.x, self.y, 0, u, v, 8, 8, 0) + class Robot(BaseMob): "Mobs that walk, without falling from cliffs, making then harder to defeat." + direction = -1 def update(self): self.dx = self.direction if is_wall(self.x, self.y + 8) or is_wall(self.x + 7, self.y + 8): - if self.direction < 0 and ( - is_wall(self.x - 1, self.y + 4) or not is_wall(self.x - 1, self.y + 8) - ): + if self.direction < 0 and (is_wall(self.x - 1, self.y + 4) or not is_wall(self.x - 1, self.y + 8)): self.direction = 1 - elif self.direction > 0 and ( - is_wall(self.x + 8, self.y + 4) or not is_wall(self.x + 7, self.y + 8) - ): + elif self.direction > 0 and (is_wall(self.x + 8, self.y + 4) or not is_wall(self.x + 7, self.y + 8)): self.direction = -1 self.dy = min(self.dy + 1, 3) self.x, self.y, self.dx, self.dy = push_back(self.x, self.y, self.dx, self.dy) @@ -356,8 +364,10 @@ def draw_template(self): v = random.choice([48, 56]) pyxel.blt(self.x, self.y, 0, u, v, 8, 8, 0) + class SlimehornBase(BaseMob): "Base class for slimehorns (see below)." + imgs = [tuple(), tuple()] def __init__(self, x, y, yzero=None, variant=False): @@ -382,20 +392,28 @@ def draw(self): combo = self.imgs[0] if self.variant else self.imgs[1] pyxel.blt(self.x, self.y, 0, combo[0], combo[1], 8, 8, 0) + class Slimehorn1(SlimehornBase): "Mobs that stick to a surface (Down)." + imgs = [(32, 48), (48, 48)] + class Slimehorn2(SlimehornBase): "Mobs that stick to a surface (Up)." + imgs = [(32, 56), (48, 56)] + class Slimehorn3(SlimehornBase): "Mobs that stick to a surface (Left)." + imgs = [(40, 48), (56, 48)] + class Slimehorn4(SlimehornBase): "Mobs that stick to a surface (Right)." + imgs = [(40, 56), (56, 56)] @@ -404,6 +422,7 @@ class Slimehorn4(SlimehornBase): class Bullet: "A bullet sent by either Diddi or Eli, which may damage enemies." + alive = False def __init__(self, x, y, r_facing=True): @@ -432,6 +451,7 @@ def draw(self): class Coin: "A coin that gives you points to brag about." + alive = False def __init__(self, x, y): @@ -448,10 +468,13 @@ def draw(self): return pyxel.blt(self.x, self.y, 0, 0, 8, 8, 8, 0) + # === Clouds === + class Cloud: "A sprite that's drawn in the background, usually representing clouds or smoke." + alive = False def __init__(self, x, y, draw_x, draw_y): @@ -461,20 +484,22 @@ def __init__(self, x, y, draw_x, draw_y): self.draw_y = draw_y self.alive = True self.speed = random.randint(2, 3) - + def update(self): self.x -= self.speed if self.x <= 0: self.alive = False - + def draw(self): if not self.alive: return # NOTE: Clouds are all stored at resource image 1, take that in count! pyxel.blt(self.x, self.y, 1, self.draw_x, self.draw_y, 16, 16, 0) + # === Button === + class Button: """ Buttons are a special and invisible NPCs whose function is to "mark" @@ -486,7 +511,7 @@ class Button: def __init__(self, x, y): self.x = x self.y = y - + def update(self): pass @@ -496,8 +521,10 @@ def draw(self): # === Base level (removed from troubled 'src.baseclasses') === + class BaseLevel(ABC): "Base level." + # tilemap = 0 player_choice = 0 # 0 is Diddi, 1 is Eli, and 2 is multiplayer player = list() # Amount of players involved @@ -572,7 +599,7 @@ def create_characters(self): elif self.player_choice == 2: self.player = [ Player1(0, self.draw_v, self.draw_v), - Player2(0, self.draw_v + 10, self.draw_v) + Player2(0, self.draw_v + 10, self.draw_v), ] def spawn(self, left_x, right_x): @@ -588,13 +615,13 @@ def spawn(self, left_x, right_x): mobclass = self.enemy_template[key] if "Slimehorn" in str(mobclass): # If we are creating a Slimehorn, let's just - # define the variant we're using :) + # define the variant we're using :) self.enemies.append( mobclass( x * 8, y * 8, self.draw_v, - self.slimehorn_variant # MAGIC :D + self.slimehorn_variant, # MAGIC :D ) ) else: @@ -614,20 +641,13 @@ def generate_clouds(self, right_x): # return draw_comb = random.choice(self.acceptable_clouds) selected_y = random.randint(self.draw_v, self.draw_v + 90) - self.clouds.append( - Cloud( - right_x, - selected_y, - draw_comb[0], - draw_comb[1] - ) - ) + self.clouds.append(Cloud(right_x, selected_y, draw_comb[0], draw_comb[1])) self.already_spawned_cloud = right_x - + def get_coin_count(self): "just return the coin count for further usage." return TOTAL_COINS - + def get_scroll_x(self): "return scroll_x for external purposes." # TODO: get rid of this workaround @@ -643,26 +663,26 @@ def update_template(self): p.update() for c in self.coins: c.update() - if c.x in range(p.x-4, p.x+8) and c.y in range(p.y-4, p.y+8) and c.alive: - TOTAL_COINS += 1 - c.alive = False + if c.x in range(p.x - 4, p.x + 8) and c.y in range(p.y - 4, p.y + 8) and c.alive: + TOTAL_COINS += 1 + c.alive = False for b in p.bullets: b.update() for e in self.enemies: if not e.alive or not b.alive: continue - if b.x in range(e.x-4, e.x+8) and b.y in range(e.y-4, e.y+8): + if b.x in range(e.x - 4, e.x + 8) and b.y in range(e.y - 4, e.y + 8): # collision between mob and bullet e.alive = False b.alive = False break for e in self.enemies: if e.alive: - if e.x in range(p.x-4, p.x+8) and e.y in range(p.y-4, p.y+8): + if e.x in range(p.x - 4, p.x + 8) and e.y in range(p.y - 4, p.y + 8): p.alive = False if p.alive and self.ending_button is not None: - if self.ending_button.x in range(p.x-4, p.x+8): - if self.ending_button.y in range(p.y-4, p.y+8): + if self.ending_button.x in range(p.x - 4, p.x + 8): + if self.ending_button.y in range(p.y - 4, p.y + 8): self.finished = True break if not self.check_anyone_alive(): @@ -685,7 +705,7 @@ def draw_template(self): gradient(self.gradient_height, self.gradient_skips), scroll_x, self.draw_v, - self.gradient_color + self.gradient_color, ) if self.check_anyone_alive(): pyxel.camera() diff --git a/src/levels.py b/src/levels.py index 5a27c8e..b83b8b1 100644 --- a/src/levels.py +++ b/src/levels.py @@ -1,14 +1,6 @@ "Library containing all the level classes, which honestly are pretty simple." - -from .characters import ( - BaseLevel, - Onion, - Robot, - Button, - Slimehorn1, - Slimehorn2 -) +from .characters import BaseLevel, Onion, Robot, Button, Slimehorn1, Slimehorn2 class TestLevel(BaseLevel): @@ -35,6 +27,7 @@ class One(BaseLevel): Mobs (9): Onions (7), Robots (2) """ + enemy_template = { "120 88": Onion, "264 80": Onion, @@ -45,7 +38,7 @@ class One(BaseLevel): "632 0": Onion, "696 56": Onion, "760 32": Robot, - "912 48": Robot + "912 48": Robot, } coin_template = [ "32 80", @@ -80,13 +73,13 @@ class One(BaseLevel): "832 32", "840 40", "848 48", - "856 56" + "856 56", ] bgcolor = 12 acceptable_clouds = [ (0, 0), (0, 0), # Augment the chances of getting a big cloud ;) - (16, 0) + (16, 0), ] ending_button = Button(1064, 96) finished_next = "two" @@ -97,18 +90,7 @@ class One(BaseLevel): use_gradient = True gradient_height = 16 gradient_color = 6 - gradient_skips = [ - 15, - 14, - 12, - 11, - 9, - 8, - 6, - 5, - 3, - 2 - ] + gradient_skips = [15, 14, 12, 11, 9, 8, 6, 5, 3, 2] class Two(BaseLevel): @@ -120,6 +102,7 @@ class Two(BaseLevel): Mobs (12): Onions (6), Robots (6). """ + draw_v = 128 enemy_template = { "160 184": Onion, @@ -134,7 +117,7 @@ class Two(BaseLevel): "952 176": Robot, "1072 216": Onion, "1088 216": Onion, - "1104 216": Onion + "1104 216": Onion, } coin_template = [ "48 200", @@ -171,7 +154,7 @@ class Two(BaseLevel): "984 168", "992 160", "1000 152", - "1008 160" + "1008 160", ] bgcolor = 5 acceptable_clouds = [(16, 16)] # Only one kind of clouds @@ -192,6 +175,7 @@ class Three(BaseLevel): Mobs (16): Onions (6), Robots (3), Desert Slimehorns (7). """ + draw_v = 256 enemy_template = { "104 320": Onion, @@ -209,7 +193,7 @@ class Three(BaseLevel): "968 328": Onion, "1120 312": Slimehorn1, "1144 328": Slimehorn1, - "1360 312": Onion + "1360 312": Onion, } coin_template = [ "168 336", @@ -265,7 +249,7 @@ class Three(BaseLevel): "1272 304", "1280 296", "1288 296", - "1296 304" + "1296 304", ] bgcolor = 14 acceptable_clouds = [(32, 0)] @@ -276,22 +260,7 @@ class Three(BaseLevel): use_gradient = True gradient_color = 9 gradient_height = 60 - gradient_skips = [ - 58, - 57, - 56, - 54, - 53, - 52, - 50, - 49, - 47, - 46, - 44, - 43, - 41, - 40 - ] + gradient_skips = [58, 57, 56, 54, 53, 52, 50, 49, 47, 46, 44, 43, 41, 40] class Four(BaseLevel): @@ -304,6 +273,7 @@ class Four(BaseLevel): Mobs (21): Onions (3), Robots (9), Icy Slimehorns (9). """ + draw_v = 384 enemy_template = { "72 424": Robot, @@ -324,7 +294,7 @@ class Four(BaseLevel): "856 416": Robot, "896 440": Slimehorn2, "1104 432": Robot, - "1344 424": Slimehorn1 + "1344 424": Slimehorn1, } coin_template = [ "40 408", @@ -381,7 +351,7 @@ class Four(BaseLevel): "1152 424", "1336 432", "1344 432", - "1352 432" + "1352 432", ] bgcolor = 5 acceptable_clouds = [(0, 32), (16, 32)] diff --git a/src/menu.py b/src/menu.py index 23381ca..dbe0c5f 100644 --- a/src/menu.py +++ b/src/menu.py @@ -3,13 +3,19 @@ from .characters import BaseLevel from .tools import draw_text, get_savedata + class Menu(BaseLevel): "Menu window." + saved_stage = "" stage = "main" player_choice = 0 enemy_template = dict() - player_choice_text = {0: "[1] Single (Diddi)", 1: "[2] Single (Eli)", 2: "[3] Multiplayer"} + player_choice_text = { + 0: "[1] Single (Diddi)", + 1: "[2] Single (Eli)", + 2: "[3] Multiplayer", + } music_vol = 5 reset_coin_counter = True gen_clouds = False @@ -19,7 +25,7 @@ def __init__(self, player_choice=None): BaseLevel.__init__(self, player_choice) # and here comes the funny part: get and save level data self.update_saved_stage() - + def update_saved_stage(self): self.saved_stage = get_savedata() @@ -82,10 +88,9 @@ def draw(self): draw_text("== Select mode ==", 23, 33) for k, v in self.player_choice_text.items(): if k == self.player_choice: - draw_text(v + " <-", 23, 45+(8*k)) + draw_text(v + " <-", 23, 45 + (8 * k)) else: - draw_text(v, 23, 45+(8*k)) + draw_text(v, 23, 45 + (8 * k)) draw_text("- Press R to return -", 23, 82) # Always remind the users how to quit draw_text("- Press Q to quit -", 23, 90) - diff --git a/src/tools.py b/src/tools.py index 202c125..e9c3d6d 100644 --- a/src/tools.py +++ b/src/tools.py @@ -14,49 +14,57 @@ "four", "preboss", "five", - "final" + "final", ) + def check_savedata(data): "Internal function to avoid warped/incorrect save data." if data["level"] not in POSSIBLE_LEVELS: data["level"] = "intro" return data + def draw_text(text, x, y, *, maincol=7, subcol=1): "Draw a pretty text on the screen." pyxel.text(x, y, text, subcol) - pyxel.text(x+1, y, text, maincol) + pyxel.text(x + 1, y, text, maincol) + def init_class(obj, popt): "Initialize a class and return the object." # TODO: Find a better way to do this? return obj(popt) + def get_savedata(): "Read and return the save data." with io.open("savedata.json", "r") as js: load = json.loads(js.read()) return check_savedata(load) + def write_savedata(data): "Write the save data from scratch." with io.open("savedata.json", "w") as js: new_data = check_savedata(data) js.write(json.dumps(new_data, sort_keys=True)) + class InternalOperationCrash(Exception): """ custom exception for internal errors with internal stuff that could only crash under testing circumstances. """ + def report_crash(opname, original): raise InternalOperationCrash( f"Error: Internal operation '{opname}' showed unexpected behavior. " f"If you are not testing this operation, please report this error. ('{original}')" ) + def gradient(height, skips): "Generate a list-of-lists needed to draw a gradient on the background." final = dict() @@ -66,31 +74,31 @@ def gradient(height, skips): continue if i % 2 == 0: # variant 1 - final[128-i] = [0 + (2*op) for op in range(0, 65)] + final[128 - i] = [0 + (2 * op) for op in range(0, 65)] else: # variant 2 - final[128-i] = [1 + (2*op) for op in range(0, 65)] + final[128 - i] = [1 + (2 * op) for op in range(0, 65)] return final + def draw_gradient(grad, ini_x, ini_y, col): # draw a given gradient data. for k, v in grad.items(): for vv in v: try: - pyxel.pset((ini_x-1) + vv, ini_y + k, col) + pyxel.pset((ini_x - 1) + vv, ini_y + k, col) except Exception: pass # this shouldn't happen anyway + def get_player_names(choice): - "Get a proper text to refer to the player pack." - ideas = ["Diddi", "Eli", "Diddi & Eli"] - try: - return ideas[choice] - except (TypeError, IndexError, ValueError) as exc: - report_crash( - f"tools.get_player_names (player_choice = {choice})", - str(exc) - ) + "Get a proper text to refer to the player pack." + ideas = ["Diddi", "Eli", "Diddi & Eli"] + try: + return ideas[choice] + except (TypeError, IndexError, ValueError) as exc: + report_crash(f"tools.get_player_names (player_choice = {choice})", str(exc)) + def draw_stats(x, y, player_selection, coins, level): "Draw a stats bar in the bottom of the screen."