logo

Maimories

Maimories

Overview

mAImories is a memory game with a twist. Instead of matching tiles based only on what you saw, you use an AI-generated riddle to figure out which emoji pair to uncover.

I built it as a small game that uses an LLM as part of the core gameplay loop, not just as a side feature. The goal was to take a familiar format, add one meaningful mechanic, and make the whole experience feel polished and replayable.

What started as a lightweight experiment turned into a complete game with progression, difficulty tuning, category filters, persistent stats, and a state model that stays clean even as the game flow gets more involved.

Core Gameplay

The game loop is simple, but it creates a different kind of challenge than a standard memory game.

At the start of each level, all tiles are revealed for five seconds. Then they hide, and the player is shown a riddle describing one specific emoji. The player has to remember where that emoji appeared and select the two matching tiles. A correct answer clears the pair and moves the level forward. A wrong answer costs a life.

The game runs across eight levels, with each level increasing the number of pairs. That means the difficulty ramps naturally as the board gets harder to memorize, while the riddle system adds a second layer of interpretation on top of the visual memory challenge.

That combination is what makes the game work. It is not just about remembering where tiles were. It is about remembering them, interpreting the clue correctly, and making the right match under pressure.

AI Integration

The most interesting part of the project is how AI is used to drive the actual gameplay.

For each board, the game generates riddles that describe the emojis the player needs to find. These are created with the Vercel AI SDK and validated with Zod schemas into typed Riddle objects, so the game is not just accepting raw model output. The AI response has to match a defined contract before it can be used.

A big part of the work here was prompt design. Each difficulty level has its own target style:

  • Easy riddles are short and direct
  • Medium riddles introduce more wordplay and metaphor
  • Hard riddles are longer, more layered, and more cryptic

This gave me a way to scale difficulty without changing the core rules of the game. The board size increases with each level, while the riddle style changes based on the selected difficulty tier.

One of the key challenges was keeping riddles interesting without making them unfair. Too literal, and the game loses its tension. Too obscure, and the player feels like they are guessing. Getting that balance right took iteration.

State and Game Flow Architecture

The game state is managed with Zustand, using a four-slice store that separates settings, current level state, overall progress, and user data.

That structure made it easier to keep the game flow predictable. Settings like difficulty and emoji category live separately from transient level state, while longer-term progress and user stats can be persisted without mixing concerns.

I also used composable selectors and focused hooks for things like score, lives, timing, and level progress. That kept UI components simple and avoided unnecessary re-renders as the board updated.

Some parts of the store are persisted to local storage, while others are intentionally ephemeral. That split let me preserve the right user data across sessions without turning the store into a dumping ground for everything.

Progression and Difficulty Design

The difficulty system does two things at once.

First, levels increase the number of pairs on the board. Level 1 starts small, and each level adds more to remember. By the later levels, the memory challenge becomes much more demanding.

Second, the selected difficulty changes how the riddles are written. Easy prompts aim for simple and direct descriptions. Medium prompts allow more playful language. Hard prompts push toward more indirect and layered clues.

That meant difficulty was not just a numbers problem. It was also a language problem. I had to think about how much ambiguity feels fun and how much just feels frustrating.

This ended up being one of the more interesting design problems in the project, because the challenge curve depends on both visual memory and prompt quality.

Stats and Replayability

I wanted the game to feel worth replaying, so I built a stats system that tracks performance over time.

The game keeps track of:

  • high scores by difficulty and emoji category
  • total games played
  • average completion time
  • per-game events and level outcomes

That made it possible to compare performance across different play styles and added a reason to come back beyond just clearing the board once.

Emoji category filters also help with replayability. Changing the category changes the feel of the board, while the riddle system keeps each run from feeling too repetitive.

UI and Experience

The UI is built with Next.js using the App Router, along with Tailwind CSS and shadcn/ui.

The visual goal was clarity. The board needed to be readable, the flow between reveal and riddle phases needed to feel obvious, and the pace had to stay quick enough that the game remained fun instead of feeling like a demo of AI output.

Dark mode support is included, and the interactions are intentionally straightforward. This is a small game, so the UI works best when it stays out of the way and lets the game loop carry the experience.

Challenges and Tradeoffs

Controlling AI output
The biggest challenge was making generated riddles consistently usable. I solved that by narrowing the prompt scope, defining difficulty-specific expectations, and validating the output against a typed schema before it reached the game.

Balancing two kinds of difficulty
The game gets harder through both board size and riddle complexity. Tuning those together was more interesting than tuning either one alone.

Keeping the state model clean
Even though the game is relatively small, it still has settings, progression, scoring, lives, timers, and persistent stats. Splitting those concerns clearly in the Zustand store made the code much easier to reason about.

Protecting the simplicity of the idea
It would have been easy to keep adding more modes and mechanics. I chose instead to keep the concept focused and make the core loop feel solid.

Tech Stack

  • Next.js (App Router)
  • TypeScript
  • Zustand
  • Tailwind CSS + shadcn/ui
  • Vercel AI SDK
  • Zod

This project shows how I like to build smaller products. Start with a simple game loop, use the LLM as a real mechanic instead of a gimmick, and make the surrounding systems clean enough that the idea can shine.