Post

Mario Bros in Python with Pyxel

Building a first‑level clone of Super Mario Bros using Python and the Pyxel library.

Mario Bros in Python with Pyxel

About the project

This project is a clone of the first level of Super Mario Bros that I built for the Programming course at UC3M. The game is written in Python using the Pyxel retro game engine. The goal was to apply OOP principles, use external libraries, and practice clean, modular code in a medium‑sized project.

Implementation

Class design

A key part of the implementation was designing classes for the different game entities (player, enemies, coins, blocks, etc.) following OOP and keeping a clear, modular structure. We created (among others) the following classes:

classDiagram
    Atrezzo <|-- Nube
    Atrezzo <|-- Arbusto
    Atrezzo <|-- Montaña

    Bloque <|-- Suelo
    Bloque <|-- Escalera
    Bloque <|-- Tuberia

    NPC <|-- Goomba
    NPC <|-- KoopaTroopa

    class Atrezzo{
        -coord: Tuple[int, int]
        -sprite: pygame.Surface
        +__init__(coord: Tuple[int, int], sprite: pygame.Surface)
    }
    class Bloque{
        -coord: Tuple[int, int]
        -sprite: pygame.Surface
        -ancho: int
        -alto: int
        -v_y: int
        -existe: bool
        +__init__(coord: Tuple[int, int], sprite: pygame.Surface)
        +reposicionar()
    }
    class NPC{
        -coord: Tuple[int, int]
        -sprite: pygame.Surface
        -ancho: int
        -esta_vivo: bool
        -v_x: int
        -v_y: int
        -ancho: int
        -alto: int
        -caparazon: bool
        +__init__(coord: Tuple[int, int], sprite: pygame.Surface)
        +sufrir_gravedad()
        +colisionar_bloque(bloques: list)
        +colisionar_npcs(npcs: list)
        +colisionar_objetos(objetos: list)
        +actualizar_posicion()
        +colisionando(entidad)
    }

Note: For brevity, inherited classes (and the base Objeto and Player) are not shown in the diagram. You can see the complete code in the repository: project source code.

Game

This is the main class and the only file that must be executed. Initially, we considered separate Menu and Level classes, but we integrated both into Game to reduce complexity and improve readability.

update

Handles three states: start menu, death menu, and level. In the death menu, pressing Enter calls reset_level() unless all lives are lost, in which case it calls reset_game(). During level execution, it calls the update methods of each class, runs keep_player_in_view() and delete_entities().

draw

Draws everything on screen. It follows the same three states as update. For performance, it only draws items that are within (or close to) the viewport, using a predicate similar to:

1
2
if not self.item_a_dibujar[i].coord[0] > 1.5 * pyxel.width:
    ...

After that, it draws element by element (with for loops), then the player and star effects (if any), and finally the HUD.

Generate ground, objects, set‑dressings, blocks and NPCs

Five helper methods create lists with positions of level elements. Ground is generated separately (despite also being blocks) because it’s easier and clearer to produce with while loops.

Keep player in view & scroll level

Tracks player coordinates; once the player reaches the middle of the screen it calls scroll_level() so the world moves around the player by adjusting object coordinates.

Rounding

Working with fractional velocities gives better game feel (inertia, smooth jumps, etc.), but it produces decimal coordinates. Python’s round can be asymmetric (e.g., round(1.5) == 2 and round(2.5) == 2). We implemented a small wrapper that subtracts an epsilon before rounding to keep visuals consistent and avoid gaps.

With roundingWithout rounding

reset_level & reset_game

reset_level restarts the level preserving only lives; reset_game resets everything. We avoided re‑calling Game.__init__ to prevent Pyxel from re‑initializing and spawning extra windows.

Player

This is the most important and busiest class.

  • Block collisions: first checks Y collisions (prevent falling through platforms and handle head bumps), then X collisions (avoid passing through walls). Standing on a block snaps the player to its top to prevent partial penetration at high speeds (also enables automatic step‑ups). Hitting a block from below temporarily doubles gravity to ensure the player doesn’t clip through.
  • NPC collisions: detects collisions and whether they’re vertical or horizontal. Vertical collisions call npc.colisionar_jugador, horizontal collisions trigger recibir_daño. When in star state, any collision kills the NPC. After a vertical hit, the player gets 15 frames of invulnerability to avoid multi‑hits.
  • Object collisions: detect pickups and update player state accordingly.
  • Input handling: maps key presses to velocity, applies friction when no input is pressed for natural deceleration.
  • Animations: actualizar_animaciones, coger_bandera, fase1_bandera, and fase2_bandera switch sprites based on state (walk/jump/flag slide), e.g.:
1
2
if pyxel.frame_count % (c.fps / n) == 0:
    cambiar_sprite()
  • Fireballs: adds a fireball object in the facing direction.
  • reset_state: resets attributes at level start and on death.

Issues encountered

Collisions

Designing a collision system that is coherent, efficient, and general‑purpose was the main challenge. We took inspiration from AABB systems but ended up with a hybrid approach tailored to our needs.

Animations

Interactions between animations and collisions caused several issues—especially with mushrooms and other objects that spawn from blocks.

1
2
3
4
5
6
class game():
    ...
class menu(game):
    ...
class nivel(game):
    ...

We initially planned menus as a separate class hierarchy, but it caused too many low‑level issues. We decided to keep everything inside the Game class for stability and simplicity.

Conclusion

This is an excellent first OOP project in Python: it forces you to organize code properly to manage many different entities without chaos. A solid first game project in Python.

This post is licensed under CC BY 4.0 by the author.