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).
Requires only JDK 17. No Android Studio, no manual sync steps.
chmod +x gradlew
./gradlew assembleDebugThe APK lands at app/build/outputs/apk/debug/app-debug.apk. Install it with:
adb install app/build/outputs/apk/debug/app-debug.apkPush 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.
| 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.
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
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.
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).
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.
-
settings.gradle.ktspresent,pluginManagement+dependencyResolutionManagementset - Root
build.gradle.ktsandapp/build.gradle.ktspresent -
gradle/libs.versions.tomlversion catalog matches every dependency alias used -
gradle.propertiessetsuseAndroidX,jvmargs,nonTransitiveRClass -
gradle/wrapper/gradle-wrapper.propertiespinned to Gradle 8.7 -
gradle/wrapper/gradle-wrapper.jarembedded (real binary, fetched from the official Gradle repo, verified as a valid zip/jar) -
gradlew/gradlew.batare the genuine upstream scripts,gradlewmarked executable -
AndroidManifest.xmlhas nopackageattribute, usesnamespacein 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.jsonhas exactly 100 entries; everymazeIdit references exists inMazeLibrary -
.github/workflows/android.ymlpresent 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