Master Pacman Rules in Unison: Complete Learning Path

black flat screen tv on brown wooden table

Master Pacman Rules in Unison: Complete Learning Path

Implementing game logic like Pacman Rules in Unison involves defining immutable game states and using pure functions for state transitions. This approach leverages Unison's powerful type system, pattern matching, and abilities to create predictable, testable, and maintainable code, perfectly suited for complex rule-based systems.

You’ve spent hours, maybe even days, wrestling with a bug in your game's state management. A player’s score mysteriously resets, a ghost phases through a wall it shouldn't, and tracking down the source feels like chasing a phantom through thousands of lines of mutable state and complex object interactions. This is the classic nightmare of imperative game development, where state can change anywhere, at any time, leading to unpredictable and chaotic behavior.

What if you could build your game logic with the certainty of a mathematical proof? Imagine a world where every action—every move, every collision, every power-up—is a predictable transformation from one valid state to the next. This is the promise of Unison. By embracing functional purity and immutability, we can escape the chaos and build robust, bug-resistant game logic. This guide will walk you through mastering the "Pacman Rules" module from the exclusive kodikra.com curriculum, transforming how you think about and implement complex stateful applications.


What Are the "Pacman Rules"? A Functional Perspective

At its core, "Pacman Rules" isn't just about a classic arcade game; it's a perfect case study for managing discrete state transitions. The game operates on a simple, yet powerful, set of rules: Pacman moves, ghosts move, pellets are eaten, scores increase, and game states change from "playing" to "won" or "lost." From a programming standpoint, this translates into a series of transformations applied to a central GameState object.

In an object-oriented paradigm, you might have a Pacman object with a move() method that mutates its own x and y coordinates. A Ghost object would do the same. The complexity arises when these objects interact. Does the Pacman object check for collisions, or does a central GameManager? Who is responsible for updating the score? This distribution of responsibility often leads to tangled dependencies and bugs.

The Unison approach, rooted in functional programming, is fundamentally different. We define the entire game world as a single, immutable value—let's call it GameState. Every action, like "move left" or "ghosts advance," is a pure function that takes the current GameState as input and returns a brand new, updated GameState. The original state is never changed, only replaced. This makes the entire system incredibly easy to reason about, test, and even debug by simply inspecting the sequence of states.


-- In Unison, we define the state with types.
-- This is a simplified example from the kodikra module.

type Direction = North | South | East | West

type Player = {
  position: (Int, Int),
  direction: Direction,
  lives: Nat
}

type Ghost = {
  position: (Int, Int),
  vulnerable: Boolean
}

type GameState = {
  player: Player,
  ghosts: [Ghost],
  pellets: [(Int, Int)],
  score: Nat,
  isWon: Boolean,
  isLost: Boolean
}

This declarative structure is the foundation. We aren't telling objects how to change themselves; we are describing what the next state of the world looks like based on an action and the current state.


Why Use Unison for Game Logic? The Purity Advantage

Choosing a language for a project is about more than just syntax; it's about choosing a philosophy. Unison's philosophy of immutability, purity, and content-addressable code offers compelling advantages for implementing rule-based systems like Pacman.

1. Predictability and Testability

A pure function, given the same input, will always return the same output and have no observable side effects. When your movePlayer function is pure, you can be 100% certain that it only affects the game state it returns. It won't secretly modify a global score variable or write to a log file. This makes testing trivial. To test a move, you simply create a starting GameState, call the function, and assert that the returned GameState is what you expect. No complex mocks or environment setup needed.


-- A pure function for state transition.
-- It takes a state and returns a new state.
eatPellet : (Int, Int) -> GameState -> GameState
eatPellet pelletPosition currentState =
  -- Create a new state based on the old one
  newScore = currentState.score + 10
  newPellets = List.filter (p -> p != pelletPosition) currentState.pellets
  { currentState with score = newScore, pellets = newPellets }

2. Simplified Concurrency and Parallelism

Since data in Unison is immutable, you never have to worry about race conditions or deadlocks caused by multiple threads trying to modify the same piece of data. The GameState value can be safely shared across different processes without any need for locks or mutexes. While a simple Pacman game might not be heavily concurrent, this feature becomes invaluable as systems grow in complexity.

3. Refactoring with Confidence

Unison's content-addressable nature, where every function is identified by a hash of its content, is a superpower for refactoring. If you rename a function or move it to a different namespace, Unison knows it's the exact same code. This eliminates the fragile, name-based linking that plagues traditional systems, allowing you to restructure your codebase fearlessly as your understanding of the game logic evolves.

4. Algebraic Effects for Clean Side Effects

What about things that are inherently impure, like drawing to the screen, reading user input, or generating random numbers? Unison handles this with a powerful feature called abilities. Abilities allow you to separate the pure core logic of your application from the impure actions. Your game logic can "request" an action (e.g., "draw the player at this position"), and a separate piece of code called a "handler" will interpret that request and perform the actual side effect. This keeps your core logic pure and testable while still allowing interaction with the outside world.


How to Implement Pacman Logic in Unison

Let's dive into the practical steps of building the Pacman rules, following the structure encouraged by the kodikra.com learning path. The process revolves around defining the data structures and then the functions that transform them.

Step 1: Define the Core Game State

As shown previously, the first step is always to model your domain with Unison's type system. A robust GameState type is your single source of truth. It should contain everything needed to render a single frame of the game and determine the outcome.

Step 2: Create Pure State Transition Functions

For every possible action in the game, create a pure function. The most fundamental one is the main game loop tick, which advances the state by one frame.


-- A function that represents one "tick" of the game loop.
gameTick : GameState -> GameState
gameTick currentState =
  -- 1. Move the player
  stateAfterPlayerMove = movePlayer currentState.player.direction currentState

  -- 2. Move all ghosts
  stateAfterGhostMoves = List.foldl (g -> accState -> moveGhost g accState) stateAfterPlayerMove currentState.ghosts

  -- 3. Check for collisions and interactions
  finalState = checkCollisions stateAfterGhostMoves

  finalState

Notice the functional composition. We are threading the GameState through a series of pure transformations. This creates a clear, auditable trail of how the state evolves.

Step 3: Use Pattern Matching for Rules and Logic

Pattern matching is Unison's Swiss Army knife for handling different scenarios. It's far more powerful and safe than a series of if/else statements. We can use it to elegantly handle collisions.


checkCollisions : GameState -> GameState
checkCollisions state =
  playerPos = state.player.position
  
  -- Check for collisions with ghosts
  collidedGhost = List.find (g -> g.position == playerPos) state.ghosts
  
  match collidedGhost with
    Some ghost ->
      -- A collision happened! Now check if the ghost is vulnerable.
      if ghost.vulnerable then
        -- Player eats ghost
        handleEatGhost ghost state
      else
        -- Ghost eats player
        handlePlayerDeath state
        
    None ->
      -- No ghost collision, check for pellet collision
      isAtPellet = List.member playerPos state.pellets
      if isAtPellet then
        handleEatPellet playerPos state
      else
        -- No collision at all
        state

This code is declarative and easy to read. It clearly outlines the logic for different collision scenarios without nested conditional spaghetti.

Step 4: Visualize the Game Loop

Understanding the flow of data is crucial. The game loop in a functional Unison application can be visualized as a cycle of state transformations.

    ● Start Game (Initial GameState)
    │
    ▼
  ┌──────────────────┐
  │  Main Game Loop  │
  └────────┬─────────┘
           │
           ├─→ Read User Input (e.g., Arrow Key)
           │
           ▼
  ┌──────────────────┐
  │   `gameTick`     │
  │ (Pure Function)  │
  └────────┬─────────┘
           │
           ├─→ Apply Player Move
           │
           ├─→ Apply Ghost AI Moves
           │
           └─→ Resolve Collisions
           │
           ▼
    ┌────────────────┐
    │ New GameState' │
    └───────┬────────┘
            │
            ├─→ Render the new state to the screen (Side Effect)
            │
            ▼
    ◆ Game Over?
   ╱            ╲
  Yes            No
  │               │
  ▼               ▼
[End Screen]    (Loop back to top)

This diagram shows how the pure logic inside gameTick is separated from the impure actions like reading input and rendering. The GameState is the immutable data that flows through this loop.

Step 5: Managing the Codebase with UCM

The Unison Codebase Manager (UCM) is the command-line tool you'll use to interact with your code. It's how you add, update, and test your functions.


# Starting the Unison Codebase Manager in your project directory
$ ucm

.> ls
  .
  ..
  types
  logic

.> add
-- UCM now watches for changes in your .u files and adds them to the codebase

.> test
-- UCM runs all the tests defined in your codebase

.> find GameState
1. types.GameState : Type

Working with ucm feels different from a traditional file-based system, but it provides powerful tools for managing and evolving your code with confidence, which you will master in the kodikra learning path.


Where This Pattern Applies Beyond Games

The "state machine" pattern implemented for Pacman is not just for games. It's a highly versatile architecture for any application that manages complex state. Think about it: what is a web application if not a system that transitions from one state (e.g., "logged out") to another ("logged in," "viewing profile," "submitting form") based on user actions?

  • UI State Management: This pattern is the foundation of libraries like Redux in the JavaScript world. A Unison implementation provides even stronger guarantees of correctness due to its type system and purity.
  • Business Process Workflows: Modeling a business process, like an order moving from "placed" to "paid" to "shipped" to "delivered," is a perfect fit. Each step is a pure function that validates the current state and produces the next one.
  • Data Processing Pipelines: An ETL (Extract, Transform, Load) pipeline can be seen as a series of state transformations on a dataset. Each step takes data in one form and outputs it in a new, refined form.
  • Simulations: Scientific or financial simulations that model systems over time rely on precisely this kind of step-by-step state evolution.

The Pacman Rules Learning Path on kodikra.com

To put this theory into practice, the kodikra.com curriculum provides a hands-on module designed to solidify your understanding. This is where you'll build the core logic, write tests, and see the power of functional programming firsthand.

The learning path is structured to guide you from the basic concepts to a fully functional implementation of the game's rules.

Module Progression:

  1. Core Module: Pacman Rules
    • This is the central challenge where you will implement the types and functions necessary to make the game work. You'll define the rules for movement, eating pellets, and interacting with ghosts.
    • Learn Pacman Rules step by step

By completing this module, you will not only have a working implementation of Pacman logic but also a deep, practical understanding of functional state management that is applicable to a wide range of software development problems.

Collision Logic Decision Tree

A key part of the exercise is implementing the collision logic. A mental model or diagram can be extremely helpful before writing the code. Here's how you can visualize the decision-making process for a player's move.

    ● Player attempts to move to New Position
    │
    ▼
  ◆ Is New Position a Wall?
  ╱                       ╲
 Yes (Invalid Move)       No (Valid Move)
  │                         │
  ▼                         ▼
[Return current GameState]  ◆ Is a Ghost at New Position?
                          ╱                             ╲
                         Yes                             No
                          │                               │
                          ▼                               ▼
                      ◆ Is Ghost Vulnerable?        ◆ Is a Pellet at New Position?
                     ╱              ╲              ╱                  ╲
                   Yes               No           Yes                  No
                    │                 │            │                    │
                    ▼                 ▼            ▼                    ▼
                [Eat Ghost,      [Player loses    [Eat Pellet,      [Just move to
                 Update Score,     a life,         Update Score,     New Position]
                 Remove Ghost]     Reset Pos]      Remove Pellet]

Pros, Cons, and Common Pitfalls

No approach is a silver bullet. While the functional, immutable-state pattern is powerful, it's important to understand its trade-offs and potential challenges.

Advantages vs. Disadvantages

Pros Cons
Highly Testable: Pure functions are easy to unit test without mocks. Performance Overhead: Creating new copies of state can be less performant than in-place mutation for very large or complex states.
Extremely Predictable: Eliminates entire classes of bugs related to mutable state. Learning Curve: Requires a shift in thinking for developers accustomed to object-oriented or imperative programming.
Easy to Debug: You can log the sequence of states to perfectly replay how a bug occurred. Boilerplate: "Plumbing" state through many function calls can sometimes feel verbose (though language features can mitigate this).
Concurrency-Safe: Immutable data structures are inherently thread-safe. Memory Usage: Can potentially use more memory due to not reusing/mutating data structures, though modern garbage collectors are very good at managing this.

Common Pitfalls & Best Practices

  • Pitfall: The "God" GameState Type. Avoid putting absolutely everything into a single, massive GameState type. If parts of the state are unrelated (e.g., UI state vs. core game logic state), consider keeping them in separate data structures that are managed by different functions.
  • Best Practice: Decompose Your Functions. A giant gameTick function is hard to read. Break it down into smaller, single-responsibility functions like movePlayer, moveGhosts, and checkCollisions. This makes the code more modular and easier to test.
  • Pitfall: Deeply Nested Pattern Matches. While powerful, a match statement that is five levels deep becomes unreadable. If you find yourself doing this, refactor the inner logic into a separate helper function.
  • Best Practice: Use Unison's Type System to Your Advantage. Make illegal states unrepresentable. For example, instead of using a Boolean like isGameOver, use a dedicated type like type Status = Playing | Won | Lost. This makes your function signatures more expressive and your logic safer.

Frequently Asked Questions (FAQ)

Is Unison fast enough for games?
For turn-based or logic-heavy games like Pacman, puzzle games, or strategy games, Unison's performance is more than sufficient. For high-performance, real-time 3D games, the core logic can still be written in Unison, while performance-critical rendering might be handled by a lower-level library via an ability handler. The key benefit is correctness, not raw frame rate.
How does this compare to an Entity-Component-System (ECS) architecture?
ECS is another popular pattern for game development that focuses on data composition. A functional approach is compatible with ECS. You can model your entities and components as immutable data structures, and your "systems" become pure functions that take the current set of components and produce the next set.
What if my game state is huge? Won't copying it all the time be slow?
This is a valid concern. Functional languages use a technique called "persistent data structures." When you "copy" a large data structure (like a list or map) and make a small change, you don't actually copy the entire thing. Instead, the new structure shares most of its memory with the old one, with only the changed parts being new. This makes updates surprisingly efficient.
Can I integrate Unison game logic with a graphical library like SDL or Raylib?
Yes. This is a perfect use case for Unison's abilities. Your pure game logic would request drawing actions (e.g., Graphics.drawSprite "player" (x, y)). The main application "shell" would provide a handler for this ability that calls the actual C functions in the SDL or Raylib library.
Is it hard to learn Unison if I only know object-oriented programming?
There is a learning curve, as it requires a different way of thinking about problems. However, the kodikra.com curriculum is designed to guide you through this mental shift. The concepts of data and functions are simple, and once they click, many developers find it a more straightforward and powerful way to build software.
Where does the Unison Codebase Manager (UCM) store my code?
UCM stores your code not in plain text files in a directory tree, but in a serialized, content-addressed format inside the .unison directory in your project's root. This is what allows for its powerful refactoring and code management capabilities, as it's operating on the abstract syntax tree (AST) of your code, not just the text.

Conclusion: Build with Certainty

Mastering the "Pacman Rules" module in Unison is about more than just recreating a classic game. It's an investment in a powerful mental model for building robust, maintainable, and predictable applications. By embracing immutable state and pure functions, you trade the chaotic world of side effects and hidden dependencies for the clarity and certainty of mathematical transformations.

The skills you develop here will empower you to tackle complex state management problems not just in games, but in user interfaces, data pipelines, and business logic. You'll learn to reason about your code with a new level of confidence, backed by the strong guarantees of the Unison language and its unique, content-addressed codebase.

Disclaimer: Technology is ever-evolving. The Unison code snippets and concepts discussed are based on the language features and best practices current as of this writing. Always refer to the official Unison documentation for the latest updates.

Ready to continue your journey? Explore the complete Unison Guide on kodikra.com or dive into the full Unison Learning Roadmap to see what's next.


Published by Kodikra — Your trusted Unison learning resource.