From Idea to Code - Building Rummy with Flutter - Part 2
In part 1, we built the foundation of our Rummy game with the UI and the core game engine.
Traditionally, creating a computer opponent means writing algorithms. You might implement an complex decision trees or write hundreds of lines of heuristic code. This algorithmic approach is robust but rigid. It requires you, the developer, to encode the strategy explicitly.
But what if we could trade algorithms for intelligence?
What if, instead of hard-coding how to play, we build an AI agent powered by Gemini, that plays Rummy against us. We won't teach it strategy, we will basically just hand it the cards, and say your turn.
The packages required:
dependencies:
# The core SDK for accessing Gemini models via Firebase
firebase_ai: ^3.5.0
firebase_core: ^4.2.1Note: Make sure you have initialised Firebase in your project.
What is an 'AI Agent'?
In the context of software development, an Agent is more than just a chatbot. It's a system capable of:
- Perception: Reading the environment (in our case, looking at the cards on the table).
- Reasoning: Deciding on a course of action based on a goal (winning the game).
- Action: Executing code to change the environment (drawing or discarding).
When we use a Large Language Model (LLM) like Gemini as an agent, we aren't just asking it to write a poem. We are treating it as a reasoning engine. We give it the current state of the game as a JSON object, and it "replies" by choosing which function in our code to call.
The Implementation
To make the AI play the game, we need three key components working in harmony. Let's look at them in order:
- The Engine (EngineController): The game state.
- The Brain (AppAgent): The AI service.
- The Coordinator (GameScreen): The UI layer.
1. The Engine: EngineController
The EngineController is the "rule enforcer." It holds the deck, the hands, and manages the phases (Drawing -> Discarding). It doesn't know who is playing, only what determines a valid move.
We expose methods for the AI (and the player) to interact with the game:
- setup(): Deals the cards.
- handOf(playerId): Returns the cards for a specific player (needed for the AI to "see" its hand).
- playerDrawFromStock() / playerDrawFromDiscard(): The drawing actions.
- playerDiscard(card): The ending action.
class EngineController {
// ... state variables ...
// The AI needs to see the top of the discard pile to decide if it wants it
Card? get discardTop => discardCard.isEmpty ? null : discardCard.last;
// The AI needs to know if moves are compliant with game phases
bool get canDrawFromStock => phase == GamePlayState.playing && !_hasDrawnThisTurn;
// Key Action: Drawing
void playerDrawFromStock() {
Card card = stock.draw();
hands.putIfAbsent(player, () => []).add(card);
_hasDrawnThisTurn = true;
}
// Key Action: Discarding
void playerDiscard(Card card) {
final hand = hands[player]!;
hand.remove(card);
discardCard.add(card);
_endTurn();
}
}2. The Brain: AppAgent
This is where the magic happens. The AppAgent translates the EngineController's state into a prompt for Gemini, and then translates Gemini's response back into EngineController function calls.
We configure Gemini with Function Calling definitions so it knows exactly what moves are legal.
class AppAgent {
late final ChatSession _chat;
AppAgent() {
// 1. Configure the Model
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-flash',
// ... config ...
systemInstruction: Content.text(
'You are a Rummy opponent. In ONE response emit EXACTLY two tool calls: '
'(1) draw_from_stock OR draw_from_discard, (2) discard_card. '
'RULE: Only use draw_from_discard if the top card completes or improves a run (same suit consecutive) '
'or a set (same rank, different suits). Otherwise draw_from_stock. '
'Use ONLY tool calls. No text. Stop after the discard.',
),
// 2. Define the Tools (The "Language" of our game)
tools: [
Tool.functionDeclarations([
FunctionDeclaration('draw_from_stock', 'Draw from stock.', {}),
FunctionDeclaration('draw_from_discard', 'Draw from discard.', {}),
FunctionDeclaration('discard_card', 'Discard a card.', {
'rank': Schema.integer(...),
'suit': Schema.string(...)
}),
]),
],
);
_chat = model.startChat();
}
// 3. The Play Loop
Future<String> playTurn({required EngineController engine, required int botPlayerId}) async {
// A. Perception: Serialize the game state
final state = {
'hand': engine.handOf(botPlayerId).map((c) => ...).toList(),
'discardTop': engine.discardTop?.toJson(),
'canDrawFromDiscard': engine.canDrawFromDiscard,
};
// B. Decision: Send state to Gemini
final response = await _chat.sendMessage(
Content('user', [TextPart(jsonEncode(state))])
);
// C. Action: Execute the tool calls
final calls = response.functionCalls;
for (final call in calls) {
if (call.name == 'draw_from_stock') {
engine.playerDrawFromStock();
} else if (call.name == 'discard_card') {
// ... parse arguments and find the card object ...
engine.playerDiscard(card);
}
}
return "Opponent played.";
}
}3. The Coordinator: GameScreen
Finally, the GameScreen ties it all together. It watches the game state; when the player finishes their turn, it triggers the AI.
class _GameScreenState extends State<GameScreen> {
// ...
// Triggered after the player discards
void _discardCard() {
// ... human player logic ...
// Pass control to the bot
_runBotTurn();
}
Future<void> _runBotTurn() async {
// 1. Validation
if (_engineController!.currentPlayerId != _opponentId) return;
setState(() => _gameMessage = "Opponent is thinking…");
// 2. Execution
// We await the result so the UI doesn't update until the bot is done
final summary = await _agent!.playTurn(
engine: _engineController!,
botPlayerId: _opponentId
);
// 3. Feedback
setState(() => _gameMessage = summary);
// 4. Check Win Condition
if (_engineController!.phase == GamePlayState.finished) {
_showGameResultModal(...);
}
}
}And that's about it! This approach shows how AI Agents can replace rigid, rule based systems with adaptive reasoning.
About Me
I am Zaahra, a Google Women Techmakers Ambassador who enjoy mentoring people and writing about technical contents that might help people in their developer journey. I also enjoy building stuffs to solve real life problems.
To reach me:
LinkedIn: https://www.linkedin.com/in/faatimah-iz-zaahra-m-0670881a1/
X (previously Twitter): _fz3hra
GitHub: https://github.com/fz3hra
Cheers,
Umme Faatimah-Iz-Zaahra Mujore | Google Women TechMakers Ambassador | Software Engineer