This is a hobby project I work on to learn Kotlin, so it moves at a hobby pace and I sometimes pick the more educational option over the most efficient one. PRs and issues are welcome anyway.
Issues live on Codeberg. Use one of these prefixes in the title so the tracker stays readable:
feat:something a player would noticefix:a bugchore:tooling, refactor, deps, CIdocs:README, inline docs, comments
If it makes sense, add an area label: area/app, area/core, area/server, area/ci.
Fork on Codeberg, branch off main, keep the PR small. One feature or one fix per PR.
Commit messages use the same prefixes as issue titles. Short subject (around 72 chars), body optional.
Before I'll merge:
./gradlew :app:lintis clean- ktlint is clean
- Tests for any pure-Kotlin code you added pass
- For UI changes, include a screenshot in the PR description. Light and dark mode if the change is visible.
Kotlin official style, ktlint enforces it.
- Prefer
valand pure functions, especially in domain code. - Anything in
domain/should be plain Kotlin, no Android or Compose imports. It needs to be JVM-testable and reusable by the server. - Avoid
!!. If you really need it, leave a one-line comment saying why. - Keep Composables small. Hoist state to
ViewModels.
CI is Woodpecker on Codeberg (.woodpecker/). Every push and PR runs lint and
unit tests. Pushing a v* tag builds a signed APK and AAB and attaches the
APK to the matching Codeberg release.
Signing is driven by environment variables, never a committed keystore:
NONONO_KEYSTORE- path to the.jksNONONO_KEYSTORE_PASSWORD,NONONO_KEY_ALIAS,NONONO_KEY_PASSWORD
Without them the release variant still builds, just unsigned (what F-Droid
expects). For CI these are Woodpecker secrets; the keystore is passed as a
base64 secret and decoded at build time. Keystores and keystore.properties
are gitignored.
To cut a release: bump versionCode/versionName in app/build.gradle.kts,
add a CHANGELOG.md entry and a fastlane/metadata/android/en-US/changelogs/<versionCode>.txt,
then tag vX.Y.Z and push the tag. F-Droid picks up the tag and the fastlane
metadata; the Play listing is updated from the same metadata; the Codeberg
release is created by CI.
Pictures are PNG files in app/src/main/assets/pictures/, named
<id>__<DisplayName>__<size>.png:
id: lowercase[a-z0-9-], unique.DisplayName:_renders as a space.size: exactly5,10or15, and the PNG must be exactly that many pixels wide and tall.- Filled rule: a pixel is a filled cell iff it is opaque (alpha >= 128) AND dark (luma < 0.5, luma = 0.299R + 0.587G + 0.114B over 255). Light or transparent pixels are empty.
- Constraints: no fully-empty row or column, and the puzzle must be uniquely
solvable as drawn. There is no auto-fixing;
PictureAssetsTest(run by CI via./gradlew :app:testDebugUnitTest) fails and prints every offending filename so you can tweak pixels.
To bulk-import a silhouette pack instead of drawing by hand, see
tools/pixel-import/README.md (vendor solid silhouettes, run the guarded
importer; the same gate culls anything not uniquely solvable). Detailed
multi-colour sprites do not work: they binarise into noise.
Licensing: this project is GPL-3.0-or-later. Bundled art must be CC0 /
public domain, or GPL-compatible (incl. CC-BY 3.0/4.0). Non-CC0 art needs a row in
app/src/main/assets/pictures_credits.tsv and an entry in CREDITS.md; it
then shows in Settings -> Attributions. Do not commit copyrighted or
AI-laundered third-party art.
Be kind, assume good faith, disagree about technical direction all you want, leave the personal stuff out.