Building Kadi: AI Opponents in a Flutter Card Game
All posts
flutter game-dev dart AI

Building Kadi: AI Opponents in a Flutter Card Game

August 20, 2025 8 min read

Kadi is a fast-paced Kenyan card game, somewhere between Uno and Crazy Eights, but with rules that make it genuinely strategic. I’ve played it my whole life, and I always thought it deserved a digital version that actually felt right.

So I built one. On macOS. In Flutter. With AI opponents.

Why Flutter for a desktop card game?

Flutter is primarily known for mobile, but its macOS support has matured significantly. For a card game, the Canvas-based rendering is a perfect fit: every card, animation, and visual effect is drawn programmatically with CustomPainter. No asset images, no sprite sheets, pure vector rendering that scales to any display resolution.

The real advantage? Cross-platform by default. The same codebase targets macOS today, but iOS and Android are a configuration change away.

The AI: simple rules beat deep learning

I spent two weeks considering a proper game tree search (minimax + alpha-beta pruning). Then I played fifty games against my own rule-based bot and realised the simpler approach was actually more fun to play against.

The Kadi AI uses a priority-based decision system:

CardPlay? decideTurn(List<Card> hand, Card topCard, GameState state) {
  // 1. Win immediately if possible
  final winMove = hand.winningPlay(topCard);
  if (winMove != null) return winMove;

  // 2. Play attack card if opponent is close to winning
  if (state.opponentIsClose) {
    final attack = hand.firstAttackOn(topCard);
    if (attack != null) return attack;
  }

  // 3. Play a card that reduces hand size optimally
  return hand.bestMatchFor(topCard, strategy: Strategy.minimize);
}

This produces a bot that feels challenging without being unfair. Players rarely realise it’s rule-based rather than “intelligent”, which is the right outcome for casual play.

Procedural audio

The sound design was the most unexpected problem. I didn’t want to ship with sample audio files, licensing, bundle size, and playback latency are all real concerns. Instead, every sound effect is synthesised at runtime using raw PCM generation:

Float32List generateCardSlap({double intensity = 1.0}) {
  final samples = Float32List(sampleRate ~/ 10);
  for (int i = 0; i < samples.length; i++) {
    final t = i / sampleRate;
    final envelope = math.exp(-t * 30.0);
    samples[i] = (noise() * envelope * intensity).clamp(-1.0, 1.0);
  }
  return samples;
}

Filtered white noise with a sharp exponential decay. Add some low-frequency thump and it sounds surprisingly like shuffling cards, without a single audio asset in the repo.

Custom-painted UI

Every visual element, cards, suits, numbers, hover states, animations, is drawn with Flutter’s Canvas API. This means the UI never looks slightly wrong at different display scales, and every micro-animation is precisely controlled in code.

The hardest part was the deal animation: 52 cards transitioning from a stack to dealt hands, each with its own bezier path and timing curve. Getting that to feel natural took more iteration than the AI logic.


Kadi is available on Mittolabs GitHub. If you know the game, give it a game, I’d love feedback on the AI difficulty.