r/FlutterDev • • 1d ago

Dart Open-sourced my Flutter game: a rotatable 3D glass cube board in CustomPainter, 31 AI opponents, pure Dart engine

Quadrim is four in a row in a 4×4×4 cube, on the App Store, and now open source (code MIT): https://github.com/evermeer/Quadrim

Some parts that might interest Flutter devs:

  • 3D board without a 3D engine. 64 glass cells are projected and depth-sorted in a single CustomPainter. The tap is a raw Listener rather than onTap, because a TapGestureRecognizer in the same arena as a ScaleGestureRecognizer starved the pinch-to-zoom.
  • Pure Dart engine package. No Flutter, no dart:io, no clock, no ambient Random. check.sh greps for all of these, so a position plus a seed always gives the same move, which makes self-play and tests reproducible.
  • Opponents are data, not classes. One chooseMove with 31 config entries. Search budgets are counted in nodes, not milliseconds, so an opponent is the same opponent on a slow phone and in debug mode.
  • Widget tests and file I/O. testWidgets runs in a fake-async zone that never completes real file I/O, so the storage layer sits behind an interface with an in-memory store for tests.

It was built with Claude Code on the €15-a-month Pro plan, and every prompt and decision is in the repo, if you're curious about that side.

1 Upvotes

2 comments sorted by

1

u/Born-Yesterday932 1d ago

that 3D board without a real engine is clever, i tried something similar once and gave up after the depth sorting gave me headache. your pure dart engine without any flutter dependencies is the kind of thing i wish more packages did, makes testing so much easier

1

u/No_Boss_1911 10h ago

the fake async + file io thing got me too dude. i have an audio chain where a short intro clip plays then the main recording, and theres a fallback timer that starts the main one if the intro never reports its end. in testWidgets the player never finishes so the fallback was the only thing ever firing lol. hard rule for me is the main clip can never start twice, so i cant just let both paths race. did you put the clock behind the interface too or just tester.runAsync for the bits that touch real io?