Skip to content

Repository files navigation

Pellet Rush

A complete, from-scratch retro arcade Pac-Man-style game for Android, built as a native Kotlin/Compose project: custom SurfaceView + Canvas engine, 100 hand-generated levels across 10 worlds, authentic four-ghost AI, and fully procedural graphics and audio (no binary assets anywhere in the repo).

Building

Requires only JDK 17. No Android Studio, no manual sync steps.

chmod +x gradlew
./gradlew assembleDebug

The APK lands at app/build/outputs/apk/debug/app-debug.apk. Install it with:

adb install app/build/outputs/apk/debug/app-debug.apk

Building via GitHub Actions (recommended if you don't have an Android SDK locally)

Push this repo to GitHub. .github/workflows/android.yml runs on every push, builds the debug APK with Gradle 8.7 / AGP 8.5.2 / JDK 17, and uploads it as a workflow artifact named pellet-rush-debug-apk — download it from the Actions run's Artifacts section and sideload it.

Tech stack

Language Kotlin 2.0.20
Build AGP 8.5.2, Gradle 8.7
UI Jetpack Compose (Material3, BOM 2024.09.00)
Rendering Custom SurfaceView + Canvas, no game engine framework
Persistence DataStore Preferences (JSON blob)
Serialization kotlinx.serialization
SDK compileSdk/targetSdk 35, minSdk 24

No Room, Hilt, KSP, Firebase, libGDX, or NDK — deliberately avoided so the project has zero external build-time surprises.

Project layout

app/src/main/java/com/pelletrush/
  engine/   GameEngine, TileMap, Entity, Pac, Ghost, GameState, Direction
            (pure Kotlin, zero Android imports - unit testable)
  ai/       GhostAI (per-ghost targeting), ScatterChaseScheduler
  level/    LevelConfig, LevelLoader, DifficultyCurve, MazeLibrary
  render/   GameView (loop + input), GameRenderer, SpriteFactory,
            PixelFont, Palette, CrtOverlay
  audio/    ToneGenerator (PCM synthesis), SoundManager (AudioTrack)
  data/     ProgressRepository (DataStore)
  ui/       Compose screens + MainActivity
app/src/main/assets/levels/levels.json   the 100-level campaign

How the 100 levels work

12 base maze layouts (28x31 tiles, classic arcade dimensions) live in MazeLibrary.kt as ASCII grids. Each was generated by a randomized spanning-tree + loop-carving algorithm and verified fully connected via BFS from the Pac-Man spawn tile before being embedded — see tools/gen_mazes.py if you want to regenerate or tweak them. The 100-level campaign (levels.json) reuses these 12 layouts across 10 worlds via vertical-flip / 180-degree-rotation transforms plus per-world modifiers, exactly like real arcade ports commonly reuse a small base set of layouts. The difficulty curve (ghost speed, fright duration, fruit value, pellet scoring) is a pure formula in DifficultyCurve.kt, not hand-tuned per level.

Ghost AI

Implements the four ghosts' original 1980 targeting rules in GhostAI.kt: Blinky chases directly (plus Cruise Elroy speed-up at low pellet counts), Pinky targets 4 tiles ahead of Pac (including the classic up-direction overflow quirk), Inky uses the Blinky-vector construction, and Clyde chases-then-flees based on an 8-tile distance threshold. Ghosts choose direction only at intersections by greedily minimizing distance to their target tile (no real pathfinding search - this matches the original arcade, which didn't do one either).

Known simplifications

This was built in one continuous pass without access to an Android SDK or emulator to compile/playtest against, so a few of the ten world mechanics are implemented as lighter-weight (but real, functioning) systems rather than fully bespoke per-maze content:

  • Locked gates and shifting walls toggle a couple of guaranteed-safe "pillar" wall tiles per maze rather than using hand-authored gate art.
  • Teleport pads are a fixed pair of guaranteed-open tiles rather than per-maze-authored pad placements.
  • Dark maze exposes a modifier flag for the renderer to use for a light-radius effect; Boss ghost gives Blinky a permanent speed/AI edge rather than a wholly separate AI implementation.

Multi-tunnel and speed worlds are fully real (each maze already has two independent tunnel rows; speed worlds apply a real extra ghost-speed multiplier on top of the difficulty curve).

Because this environment has no Android SDK or network access to Google's Maven repository, the build could not be compiled here. Gradle syntax, AGP/ Kotlin/Compose version compatibility, and every asset reference were checked by hand and via the self-audit below; the GitHub Actions workflow is the first real compile.

Self-audit

  • settings.gradle.kts present, pluginManagement + dependencyResolutionManagement set
  • Root build.gradle.kts and app/build.gradle.kts present
  • gradle/libs.versions.toml version catalog matches every dependency alias used
  • gradle.properties sets useAndroidX, jvmargs, nonTransitiveRClass
  • gradle/wrapper/gradle-wrapper.properties pinned to Gradle 8.7
  • gradle/wrapper/gradle-wrapper.jar embedded (real binary, fetched from the official Gradle repo, verified as a valid zip/jar)
  • gradlew / gradlew.bat are the genuine upstream scripts, gradlew marked executable
  • AndroidManifest.xml has no package attribute, uses namespace in Gradle instead
  • No reference to any missing drawable/raw asset/font - the launcher icon is a hand-authored vector, HUD font is drawn procedurally
  • levels.json has exactly 100 entries; every mazeId it references exists in MazeLibrary
  • .github/workflows/android.yml present and builds + uploads the APK
  • Dependency list matches the allowed set exactly: core-ktx, appcompat, activity-compose, compose BOM (ui/material3/tooling), lifecycle-runtime-ktx, datastore-preferences, kotlinx-serialization-json

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages