Zebra Puzzle in Arm64-assembly: Complete Solution & Deep Dive Guide


Mastering Logic Puzzles with Arm64-assembly: The Zebra Puzzle Deep Dive

The Zebra Puzzle is a classic logic problem that challenges your deductive reasoning to determine who drinks water and who owns the zebra based on a set of 15 constraints. This guide breaks down how to approach, model, and implement a solution for this complex puzzle using low-level Arm64-assembly, exploring the core concepts of computational logic and machine code.


You've probably encountered a brain-teaser that felt utterly impossible at first glance. The clues seem disconnected, the goal distant, and the path forward shrouded in fog. The Zebra Puzzle is the quintessential example of such a challenge, a benchmark for logical deduction that has captivated and confounded problem-solvers for decades.

Now, elevate that challenge. Imagine not just solving it yourself, but teaching a machine to solve it. Not with the comfortable abstractions of Python or Java, but by speaking the computer's native tongue: assembly language. Specifically, Arm64-assembly, the architecture powering everything from your smartphone to massive data centers.

This might sound like an insurmountable task, but it's one of the most rewarding journeys a programmer can undertake. It forces you to think about problems from first principles—how to represent data, how to check rules, and how to search for a solution using nothing but registers and memory. This deep dive will guide you through that very process, transforming a complex logic puzzle into a concrete, low-level algorithm.


What is the Zebra Puzzle?

At its core, the Zebra Puzzle is a Constraint Satisfaction Problem (CSP). You are given a set of variables (the attributes of five houses) and a series of constraints (the 15 rules). The goal is to assign a value to each variable in a way that satisfies all the constraints simultaneously.

The Setup: 5 Houses, 25 Attributes

The puzzle universe consists of five houses arranged in a row. Each house has five distinct attributes:

  • Color: Red, Green, Ivory, Yellow, Blue
  • Nationality: Englishman, Spaniard, Ukrainian, Norwegian, Japanese
  • Pet: Dog, Snails, Fox, Horse, Zebra
  • Beverage: Coffee, Tea, Milk, Orange Juice, Water
  • Hobby: Reading, Painting, Gardening, Playing Chess, Watching Movies (Note: these are illustrative; the original puzzle might use different attributes like cigarette brands, but the logic is identical).

The challenge arises because the connections between these attributes are not given to you directly. You must deduce them from a list of 15 clues.

The 15 Definitive Clues

The solution is discoverable only by synthesizing the information from every single one of these statements:

  1. There are five houses.
  2. The Englishman lives in the red house.
  3. The Spaniard owns the dog.
  4. Coffee is drunk in the green house.
  5. The Ukrainian drinks tea.
  6. The green house is immediately to the right of the ivory house.
  7. The person with the snail-keeping hobby owns snails.
  8. The person in the yellow house has a hobby of painting.
  9. Milk is drunk in the middle house (house #3).
  10. The Norwegian lives in the first house (house #1).
  11. The person with the chess-playing hobby lives next to the man with the fox.
  12. The person with the painting hobby lives next to the house where the horse is kept.
  13. The person who enjoys gardening drinks orange juice.
  14. The Japanese person has a hobby of reading.
  15. The Norwegian lives next to the blue house.

The Ultimate Questions

After untangling this web of logic, your program must provide the definitive answers to two questions:

  1. Which resident drinks water?
  2. Who owns the zebra?

Why Solve This in Arm64-assembly?

Solving a logic puzzle in a high-level language is a common academic exercise. But moving down the stack to Arm64-assembly is a deliberate choice that offers unparalleled insights into the fundamentals of computing. It's not about finding the easiest path to the answer; it's about understanding what "the path" is actually made of.

Peeling Back the Layers of Abstraction

Languages like Python provide powerful data structures like dictionaries and lists, along with garbage collection and dynamic typing. These are fantastic for productivity but hide what's truly happening in the machine. In assembly, there are no such luxuries. You are in direct command of the hardware.

This exercise forces you to answer fundamental questions:

  • Data Representation: How do you represent five houses and their 25 attributes using only raw memory bytes? You must design your own data structure from scratch.
  • Control Flow: How do you implement complex conditional logic ("if the house is green AND its neighbor is ivory...") using only comparison and branch instructions?
  • Algorithmic Strategy: How do you implement a search algorithm like backtracking when you don't have built-in recursion or data stacks? You must manage the call stack and state manually.

A Lesson in Performance and Precision

While performance is not the primary goal for this specific puzzle, the principles learned are directly applicable to high-performance computing, game development, and embedded systems. Writing in assembly gives you granular control over CPU registers, memory access patterns, and instruction pipelines, which is essential for squeezing every last drop of performance out of the hardware.


How to Architect a Solution: The Algorithmic Blueprint

You cannot solve this problem by simply translating each clue into a line of code. You need a systematic strategy to explore the vast space of possible solutions while adhering to the constraints. The most effective approach for this class of problem is backtracking search.

Step 1: Define the Data Structure

First, we need to decide how to store the state of the five houses in memory. A simple and effective way is to create a structure for each house. Since assembly doesn't have `structs` in the C sense, we define a memory layout. Let's say each house's data block is 5 bytes, where each byte represents an attribute (Color, Nationality, etc.).

We can use integer constants to represent the different values (e.g., `RED=0`, `GREEN=1`; `ENGLISHMAN=0`, `SPANIARD=1`).

; Memory Layout for 5 Houses (Total 25 bytes)
; House 1: [Color, Nationality, Pet, Beverage, Hobby]
; House 2: [Color, Nationality, Pet, Beverage, Hobby]
; ...
; House 5: [Color, Nationality, Pet, Beverage, Hobby]

; Example: Address `houses_data` points to House 1's Color.
; `houses_data + 5` points to House 2's Color.
; `houses_data + 1` points to House 1's Nationality.

Here is a visual representation of this memory layout:

 ● Start of `houses_data` memory block
 │
 ├─ House 1 (offset +0)
 │   ├─ Byte 0: Color
 │   ├─ Byte 1: Nationality
 │   ├─ Byte 2: Pet
 │   ├─ Byte 3: Beverage
 │   └─ Byte 4: Hobby
 │
 ├─ House 2 (offset +5)
 │   ├─ Byte 5: Color
 │   ├─ Byte 6: Nationality
 │   ...
 │
 ▼ ... and so on for Houses 3, 4, 5

Step 2: The Backtracking Algorithm

Brute-forcing every single combination of 25 attributes is computationally infeasible. The number of permutations is astronomical. Instead, we use backtracking, which intelligently prunes the search space.

The algorithm works as follows:

  1. Make a Choice: Assign a possible value to an unassigned attribute (e.g., set House 1's color to Red).
  2. Check for Violations: After each assignment, immediately check if any of the 15 rules have been violated with the current partial solution.
  3. If No Violation: Move to the next unassigned attribute and recurse (go back to step 1).
  4. If Violation Occurs: The last choice was wrong. Undo it (backtrack) and try the next possible value for that same attribute. If all values have been tried, backtrack further up the decision tree.
  5. Solution Found: If all attributes for all five houses are assigned without violating any rules, you have found the solution.

This process systematically builds a valid solution, abandoning invalid paths as soon as a conflict is detected, which saves an enormous amount of computation.

This flow can be visualized as follows:

    ● Start Solver
    │
    ▼
  ┌─────────────────────────┐
  │ Find Unassigned Variable │
  │ (e.g., House 2's Color)  │
  └────────────┬────────────┘
               │
               ▼
  ┌─────────────────────────┐
  │  Loop Through Possible  │
  │    Values (Red, Blue...)│
  └────────────┬────────────┘
               │
               ▼
    ◆  Assign Value & Check Constraints
   ╱                         ╲
  Valid Path                  Violation Found
  │                           │
  ▼                           ▼
┌──────────────────┐      ┌──────────────────┐
│ Recurse to Next  │      │ Undo Assignment  │
│     Variable     │      │ (Backtrack)      │
└─────────┬────────┘      └──────────────────┘
          │
          ▼
    ◆ All Variables Assigned?
   ╱           ╲
 Yes            No ────┐
  │                     │
  ▼                     └─(Continue Loop)
┌───────────┐
│ Solution! │
└───────────┘
    │
    ▼
  ● End

Where the Code Fits In: A Detailed Walkthrough

The solution provided in the initial kodikra.com module is a placeholder, a common technique in learning environments. It doesn't contain the backtracking logic; instead, it hardcodes the final answer. This is done to test your understanding of function definitions, calling conventions, and return values in Arm64-assembly before you embark on writing the full, complex solver.

Let's break down this provided code snippet line by line to understand exactly what it's doing.


.text
.globl drinks_water
.globl owns_zebra

/* extern char drinks_water(void); */
drinks_water:
    mov w0, #'N'
    ret

/* extern char owns_zebra(void); */
owns_zebra:
    mov w0, #'J'
    ret

Dissecting the Code

Directives

  • .text: This is an assembler directive that declares the start of the code section. It tells the assembler that the following lines are executable machine instructions, which will be placed in the text segment of the final program.
  • .globl drinks_water and .globl owns_zebra: The .globl directive (short for "global") makes a symbol visible to the linker. This means that other object files can call the drinks_water and owns_zebra functions. It's the assembly equivalent of exporting a function in a higher-level language.

The drinks_water Function

  • drinks_water:: This is a label that marks the starting address of the function. When another part of the program calls drinks_water, execution jumps to this point.
  • mov w0, #'N': This is the heart of the function.
    • mov is the "move" instruction. It copies a value from a source to a destination.
    • w0 is the destination register. In the ARM64 Procedure Call Standard (AAPCS), the w0 register (the lower 32 bits of the 64-bit x0 register) is used to hold the return value of a function if it's 32 bits or smaller. Since the function is expected to return a char (which is 8 bits), w0 is the correct register to use.
    • #'N' is the source value. The single quotes tell the assembler to use the ASCII value of the character 'N'. The ASCII value for 'N' is 89. So, this instruction effectively means "place the integer value 89 into register w0". This hardcodes the answer that the **N**orwegian drinks water.
  • ret: This instruction stands for "return". It tells the CPU to end the current function and return control to the caller. It does this by loading the program counter (PC) with the address stored in the link register (x30), which was automatically saved when the function was called.

The owns_zebra Function

This function is structurally identical to the first one.

  • owns_zebra:: A label marking the start of this function.
  • mov w0, #'J': This instruction moves the ASCII value of the character 'J' (which is 74) into the return value register w0. This hardcodes the answer that the **J**apanese person owns the zebra.
  • ret: Returns from the function to the caller.

From Stub to Solver: A Conceptual Leap

The real task is to replace these hardcoded stubs with a full backtracking solver. A complete assembly implementation would be thousands of lines long. However, we can outline the key components in pseudo-code and show how to translate small, critical parts into Arm64-assembly.

High-Level Pseudo-Code for the Solver


// Represents the 5 houses and their attributes
int houses[5][5];

// Array to track used values for each category
bool used_values[5][5];

bool solve() {
    // Find the next unassigned variable (house, attribute)
    if (!find_unassigned(row, col)) {
        return true; // All assigned, solution found!
    }

    // Try all possible values (0-4) for this variable
    for (int val = 0; val < 5; val++) {
        // Check if this value is already used in its category
        if (!used_values[col][val]) {
            houses[row][col] = val;
            used_values[col][val] = true;

            // Check if current partial assignment is valid
            if (check_constraints()) {
                if (solve()) { // Recurse
                    return true;
                }
            }

            // Backtrack: undo the assignment
            houses[row][col] = -1; // -1 for unassigned
            used_values[col][val] = false;
        }
    }

    return false; // No value worked, trigger backtrack in parent call
}

Translating Logic to Assembly Snippets

Let's look at how a simple constraint like "The Englishman lives in the red house" would be checked in assembly.

Constraint: For each house `i` from 1 to 5, if `house[i].nationality == ENGLISHMAN`, then `house[i].color` must be `RED`.


; Assume x19 points to the start of the houses_data array
; Assume x20 is our loop counter `i`, from 0 to 4
; Constants: ENGLISHMAN = 0, RED = 0, NATIONALITY_OFFSET = 1, COLOR_OFFSET = 0

check_eng_red_loop:
    ; Calculate address of current house's nationality
    ; addr = base + (i * 5) + NATIONALITY_OFFSET
    mov x21, #5
    mul x22, x20, x21      ; x22 = i * 5
    add x23, x19, x22      ; x23 = base + (i * 5)
    ldrb w1, [x23, #NATIONALITY_OFFSET] ; Load nationality byte into w1

    ; Check if nationality is Englishman
    cmp w1, #ENGLISHMAN
    b.ne continue_loop     ; If not Englishman, skip to next house

    ; If it IS the Englishman, check the color
    ldrb w2, [x23, #COLOR_OFFSET] ; Load color byte into w2
    cmp w2, #RED
    b.ne constraint_failed ; If color is not Red, we have a violation!

continue_loop:
    add x20, x20, #1       ; i++
    cmp x20, #5            ; Compare i with 5
    b.lt check_eng_red_loop ; If i < 5, loop again

    ; ... more checks or return success ...

constraint_failed:
    ; ... code to handle the backtrack ...

This snippet demonstrates the core loop of assembly programming: calculating memory addresses, loading data into registers (ldrb), comparing values (cmp), and conditionally branching based on the result (b.ne, b.lt). A full solver would consist of many such routines, all orchestrated by the main backtracking function.


Who Benefits from This Challenge?

This module from the kodikra learning path is not just for aspiring assembly programmers. It's a valuable exercise for a wide range of developers and computer scientists.

  • Computer Science Students: It provides a practical application for topics like computer architecture, data structures, and algorithm design.
  • Embedded Systems Engineers: Working in a resource-constrained environment requires a deep understanding of memory layout and CPU behavior, skills that are honed by this type of problem.
  • Performance Engineers & Game Developers: While they may not write entire applications in assembly, they often need to read and optimize assembly output from compilers to debug performance bottlenecks.
  • Curious Developers: Anyone wanting to truly understand what happens "under the hood" of their high-level code will find this exercise incredibly illuminating.

Pros and Cons of Using Assembly for Logic Puzzles

Pros Cons
Unmatched Control: You have direct control over every CPU instruction and memory access, allowing for ultimate optimization. Extreme Verbosity: Simple operations require multiple lines of code, making the source code very long and difficult to read.
Deep System Understanding: It forces you to learn about CPU registers, memory alignment, and system calling conventions. High Development Time: Implementing complex algorithms like backtracking is significantly more time-consuming and error-prone than in a high-level language.
No Hidden Abstractions: There is no garbage collector, runtime, or other hidden process. You see exactly what the machine is doing. Poor Portability: Arm64-assembly code will not run on an x86-64 processor. The code is tied to a specific architecture.
Educational Value: It provides a foundational understanding that makes you a better programmer, even in high-level languages. Difficult Debugging: Debugging involves stepping through individual instructions and watching register values, which can be tedious.

Frequently Asked Questions (FAQ)

What exactly is a Constraint Satisfaction Problem (CSP)?

A CSP is a type of problem defined by a set of variables, a domain of possible values for each variable, and a set of constraints that the variables must satisfy. The goal is to find an assignment of values to variables that satisfies all constraints. Sudoku, map coloring, and scheduling problems are all classic examples of CSPs.

Why use Arm64-assembly instead of a simpler language like Python?

The purpose is not efficiency of development, but depth of learning. Using Python would solve the puzzle quickly but would teach you little about how the computer is actually executing the logic. Arm64-assembly forces you to manage memory and control flow manually, providing a fundamental understanding of computation that is invaluable for advanced topics like systems programming and performance optimization.

What does the `mov w0, #'N'` instruction actually do at the hardware level?

This instruction directs the CPU's control unit to take the 8-bit ASCII value for the character 'N' (which is 89, or 0x59 in hex) and place it into the lower 32 bits of register x0, which is known as w0. This operation involves setting the electronic gates and latches that comprise the w0 register to store the binary pattern for 89 (01011001).

Is backtracking the only way to solve the Zebra Puzzle?

No, but it's one of the most intuitive and common for this type of problem. Other methods include logic programming (using languages like Prolog, which are designed for these problems) and converting the problem into a Boolean satisfiability problem (SAT) to be solved by a dedicated SAT solver. However, for a general-purpose programming language, backtracking is a very standard and effective approach.

How does this puzzle relate to real-world programming challenges?

The underlying principles are everywhere. Constraint satisfaction is key in logistics (e.g., vehicle routing), scheduling (e.g., creating university timetables), resource allocation in cloud computing, and even in AI for planning and diagnostics. While you might use a library to solve these problems in the real world, understanding the core algorithms like backtracking helps you use those libraries more effectively and debug them when things go wrong.

What is the ARM64 Procedure Call Standard (AAPCS)?

The AAPCS is a set of rules that govern how functions call each other on the ARM64 architecture. It defines which registers are used to pass arguments (x0-x7), which register holds the return value (x0), which registers must be preserved by a function, and which can be freely used (clobbered). Adhering to this standard ensures that code compiled from different languages (like C and assembly) can interoperate correctly.

Where can I learn more about Arm64-assembly programming?

The kodikra.com curriculum is an excellent place to start, with a structured path from basics to advanced concepts. For a broader overview and reference materials, exploring our complete Arm64-assembly guide is highly recommended. It covers the instruction set, architecture, and programming conventions in great detail.


Conclusion: From Logic to Machine Code

The Zebra Puzzle is far more than a simple riddle; it's a gateway to understanding the essence of computational problem-solving. By tackling it in Arm64-assembly, you move beyond the comfort of high-level syntax and engage directly with the machine. You learn to meticulously structure data in memory, to build complex logic from simple comparisons and branches, and to implement powerful algorithms like backtracking from the ground up.

While the initial code in this kodikra module simply provides the final answer, the true lesson lies in understanding the immense and intricate journey required to arrive at that answer programmatically. It's a testament to the layers of abstraction we rely on every day and a powerful reminder of the fundamental principles that make all modern software possible.

Technology Disclaimer: The concepts and instructions discussed (e.g., mov, ldr, cmp, ret) are fundamental to the ARMv8-A and ARMv9-A architectures (AArch64). While the core logic is timeless, always consult the latest official ARM architecture reference manuals for the most current instruction set details and conventions.

Ready to continue your journey into the heart of the machine? Explore the full Arm64-assembly 9 learning path on kodikra.com to tackle more challenges that will sharpen your low-level programming skills.


Published by Kodikra — Your trusted Arm64-assembly learning resource.