Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.11", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: pip install -e .[dev]
- name: Run tests (headless)
run: python -m pytest -q

lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install ruff
run: pip install ruff
- name: Lint
run: ruff check .
194 changes: 185 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,189 @@
# Pygame Examples
# PyGame Lunar Lander
[![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)

I always learn better from examples -- a long tutorial explaining the
background doesn't do it for me. To that end, here's a collection of
[PyGame](http://pygame.org) example games I've developed in the
process of learning PyGame.
A small arcade-style lunar landing simulation written with [PyGame](https://www.pygame.org/).

Right now there's:
Pilot the lander down to a gentle touchdown on the grey platform. Keep the
craft upright, watch your fuel, and avoid the boulders — a hard touchdown
destroys the ship.

- `snake.py`: a simple ~100-line version of Snake.
- `lunarlander.py`: a slightly-more-complicated Lunar Lander game.
## Controls

Happy coding!
| Key | Action |
|-----------------|---------------------------------|
| `SPACE` / `UP` | Fire the engine (boost) |
| `LEFT` | Rotate counter-clockwise |
| `RIGHT` | Rotate clockwise |
| `R` | Restart the game |
| close window | Quit |

## Installation

Requires Python 3.9+ and PyGame 2.1+.

```bash
pip install -e . # install the game and the `lunar-lander` command
pip install -e ".[dev]" # also install pytest and ruff for development
```

## Running the game

```bash
python -m lunarlander # or just: lunar-lander
```

## Running the tests

The test-suite runs headlessly (no window is opened; the SDL `dummy` video
and audio drivers are used automatically via `conftest.py`).

```bash
python -m pytest
ruff check . # lint
```

## Project layout

```
.
├── lunarlander/ # the game package
│ ├── __init__.py # public API re-exports
│ ├── __main__.py # `python -m lunarlander`
│ ├── constants.py # physics/terrain parameters and polled keys
│ ├── vectors.py # the `V` 2D vector type
│ ├── assets.py # image loading
│ ├── terrain.py # procedural sky/moon/boulders, `Moon`, `Boulder`
│ ├── sprites.py # the `Lander` sprite
│ ├── game.py # `LunarLander` game object + entry point
│ ├── lander.jpg # image assets
│ └── lander_flame.jpg
├── tests/
│ └── test_lunarlander.py # 51 unit + integration tests
├── conftest.py # pytest config; initialises PyGame headlessly
├── pyproject.toml # packaging, ruff and pytest configuration
└── README.md
```

## Design

The code is split into a small set of classes, each with a single
responsibility:

- **`V`** — a minimal 2D vector. Supports Cartesian and polar
initialisation, and clockwise-angle semantics where `0` = up, `90` = right.
- **`Lander`** — physics (gravity, thrust, rotation), fuel, landing
detection, and the cached animated engine flame.
- **`Moon`** — the flat, safe landing platform (`landing_ok = True`).
- **`Boulder`** — an unsafe obstacle (`landing_ok = False`); touching one
always destroys the craft.
- **`LunarLander`** — the top-level game object. Owns the screen, sprites
and the main loop. `step()` advances the simulation one frame and is kept
separate from `draw()`, so the whole game can be driven headlessly.

## Physics

Physics is modeled in real-world **SI units** and integrated with a
delta-time (`dt`) step, so the simulation behaves identically at any frame
rate:

| Quantity | Value | Description |
|--------------------|--------------|------------------------------------------|
| `GRAVITY` | `1.62 m/s²` | the Moon's surface gravity |
| `DRY_MASS` | `1500 kg` | lander mass with an empty tank |
| `FUEL_MASS` | `450 kg` | full-tank propellant mass |
| `FUEL_BURN_RATE` | `4 kg/s` | propellant flow while thrusting |
| `EXHAUST_VELOCITY` | `2500 m/s` | effective exhaust velocity |
| `ENGINE_THRUST` | `10 kN` | = burn rate × exhaust velocity |
| `LANDING_VELOCITY_LIMIT` | `3 m/s` | max safe touchdown speed |
| `PIXELS_PER_METER` | `8` | world-to-screen scale |

The rocket equation behaviour is real:

```
acceleration = (thrust − weight) / mass = (thrust − mass·g) / mass
```

As propellant burns, `mass` falls and thrust-to-weight ratio *rises* from
≈3.2 to ≈4.1 — the craft becomes progressively more responsive, exactly
like a real rocket. Coasting descends at the Moon's gravity. Position and
speed are stored in float so sub-pixel motion is never lost.

Two extra measures keep the simulation robust:

- **Tunnelling prevention** — collision detection is *swept*: if the craft
crossed the surface's top edge during the previous frame it is snapped
back on top, so it can never fall through the floor between frames.
- **Delta-time clamping** — `dt` is clamped to `MAX_DT = 0.05 s` so a slow
frame (window drag, background process) cannot teleport the craft.

## Performance

A few choices keep the game fast at 80 FPS:

- **Rotated-frame caching** — `pygame.transform.rotate` is expensive and is
only called when the orientation actually changes; every rotation is
cached by `(image, rounded angle)`.
- **Fonts are created once** — the previous version re-created a `Font`
object every single frame.
- **No redundant redraws** — the old code called `display.flip()` *and*
`display.update()` every frame; only `flip()` is needed.
- **No event flooding** — `key.set_repeat(1, 1)` (which queued hundreds of
KEYDOWN events per second) has been removed; input is polled once per
frame with `get_pressed()`.
- **In-place vector math** — `V.__iadd__` avoids allocating a new vector on
every physics update.

## Changes from the original

The original `pygame-lunarlander` was Python 2 code that no longer ran under
modern PyGame. Besides the port to Python 3, this fork fixes:

1. **`V.rotate` corrupting vectors** — `self.y` was computed with the
already-updated `self.x`.
2. **`V.angle` returning wrong quadrants** — replaced a broken
`atan(x/y)` implementation with `atan2` and normalised to `[0, 360)`.
3. **Negative orientations counted as "upright"** — `-20°` passed the old
`orientation < 10` check. Orientation is now kept in `[0, 360)`.
4. **Sprites never initialised properly** — `DirtySprite.__init__` was never
called via a broken `super()` dance; sprites now call `super().__init__()`.
5. **Boulders rendered as black squares** — the rock surface now uses
per-pixel alpha (`SRCALPHA`).
6. **Moon drawn over the landed ship** — draw order was inverted; the moon
now renders behind the lander.
7. **`time.sleep(1)` freezing the game** — the post-landing delay is now
frame-based, so the window still responds.
8. **Moon tunnelling** — see *Tunnelling prevention* above.
9. **`except pygame.error, message:` / `print`** — Python 2 syntax, converted
to Python 3.
10. **Unrealistic, uncontrollable physics** — the original gravity (0.5
px/frame²) and impulse-style engine made the craft either plummet in
under a second or rocket off-screen. Physics is now real SI rocket
mechanics (see [Physics](#physics)): Moon gravity, mass loss, rising
thrust-to-weight ratio, and a gentle `3 m/s` touchdown limit.
11. **Sub-pixel movement was lost** — `Rect.move()` truncates float offsets
to integers, so tiny per-frame motion was discarded. Position is now
accumulated in float and only rounded for rendering.
12. **Flat-circle terrain** — the surface is now a procedurally drawn lunar
landscape: starfield, cratered ground, a marked landing pad and
irregular, shaded boulders.
13. **No time to prepare** — the game now has a *ready* screen; press SPACE
to launch instead of starting mid-fall.
14. **Dead keyboard input** — `pygame.key.get_pressed()` is *scancode*-indexed,
so keys built from `enumerate(...)` never matched the `K_*` constants.
Input is now polled via the `K_*` constants in `pressed_keys`.
15. **Thrust discarded when fuel ran out mid-frame** — the last partial burn
now delivers its impulse (`thrust = burned/dt × exhaust velocity`) instead
of being dropped. Also: `V` equality uses `math.isclose`, and `load_image`
converts per-pixel-alpha images with `convert_alpha()`.
16. **Packaged as a proper module** — the single-file `lunarlander.py` was
split into a package (`constants`, `vectors`, `assets`, `terrain`,
`sprites`, `game`), with `pyproject.toml` for packaging/linting, `ruff`
linting and a GitHub Actions CI workflow (badges above).

## Contributing

- Run `python -m pytest` and `ruff check .` before pushing — all tests must
pass and lint must be clean (CI enforces both).
- Keep physics in SI units and `dt`-based so behaviour is frame-rate
independent.
- Add a test for any new behaviour in `tests/test_lunarlander.py`.
13 changes: 0 additions & 13 deletions brickout-game/README.md

This file was deleted.

Binary file removed brickout-game/classDiagram.png
Binary file not shown.
Loading