This is a Sokoban-style puzzle game developed in assembly language for the ICMC-USP processor, a simple Von Neumann architecture computer implementing a RISC instruction set. The unique feature of this game is its exploration of different 2D manifold topologies, where puzzles exist on surfaces like a torus (Pac-Man style wrapping).
Platform: ICMC-USP Processor (More Info)
Screen Resolution: 40 columns × 30 rows = 1200 character positions
Current Status: Core gameplay systems implemented including player movement, box pushing, layered rendering, and a complete UI framework.
The game follows a modular architecture with several independent systems that communicate through shared memory variables:
┌─────────────────────────────────────────────────────┐
│ Main Game Loop │
│ (Input → Update → Render) │
└──────────────┬──────────────────────────────────────┘
│
┌─────────┴─────────────────────────────┐
│ │
┌────▼─────┐ ┌──────────┐ ┌───────────┐ ┌▼────────┐
│ Input │ │ Movement │ │ Behavior │ │ UI │
│ Handler │→ │ System │→ │ System │ │ System │
└──────────┘ └────┬─────┘ └─────┬─────┘ └─────────┘
│ │
┌────▼──────────────▼────┐
│ Topology System │
└────────┬───────────────┘
│
┌────────▼───────────┐
│ Rendering System │
│ (3 Layers) │
└────────────────────┘
The game uses several key memory regions:
- Layer Buffers (1200 chars each): UI, Props, Background, Behavior
- Render Queue: Dirty rendering optimization (1210 indices)
- UI Stack: Up to 20 stacked UI elements
- Behavior Jump Table: 128 function pointers for game objects
The rendering system implements a layered, dirty rendering approach for optimal performance.
The game uses four distinct layers rendered in priority order:
- UI Layer (highest priority) - Menus, dialogs, prompts
- Props Layer - Player, boxes, walls, interactive objects
- Background Layer - Static decorative elements, titles
- Behavior Layer - Invisible collision/interaction data
Key Functions:
render()- Main render loop, dispatches to either normal or UI renderingScreenRenderIndex(r1)- Renders a single screen position from Props/Background layersScreenRenderUIIndex(r1)- Renders a single screen position from UI layerFullScreenPrint()- Forces complete screen refresh
Instead of redrawing the entire 1200-character screen each frame, the system tracks which positions have changed:
currentScreenIndexesChanged[1210] ← Buffer of changed indices
currentScreenIndexesChangedIndex ← Write pointer into buffer
How it works:
- Game logic calls
SetIndexChanged(r0)whenever a position changes - The index is added to the
currentScreenIndexesChangedbuffer - During render, only indices in the buffer are redrawn
- Buffer is cleared after rendering
Key Functions:
SetIndexChanged(r0)- Marks position r0 as needing redrawSquareFinderSetIndexChanged(r1)- Marks rectangular regions for redraw
Each layer has an associated default color:
uiLayerColor- White (0)propLayerColor- White (0)backgroundLayerColor- White (0)currentPrintingColor- Temporary color override (0 = use layer default)
Colors are added to ASCII values before output: outchar (character + color), position
Simple but effective buffered input handling.
Key Variables:
InputedChar- Current frame's processed inputInputedCharBuffered- Next frame's input (prevents input loss)
Function: InputHandler()
The input handler reads one character per frame using the inchar instruction. Input is stored directly without debouncing or delay (delay logic was moved out of this system for modularity).
The movement system handles player and box movement with support for different 2D manifold topologies.
MvRight(r0)- Add 1 to positionMvLeft(r0)- Subtract 1 from positionMvUp(r0)- Subtract 40 from position (one row)MvDown(r0)- Add 40 to position
These functions perform raw position changes without bounds checking.
Function: mvTopology(r0, r1)
Takes a position and previous position, applies topological wrapping rules, and returns the corrected position.
Current Implementation: Torus (Pac-Man Style)
The torus topology creates seamless wrapping on both axes:
Horizontal Wrapping:
Left edge (column 0) → Right edge (column 39)
Right edge (column 39) → Left edge (column 0)
Vertical Wrapping:
Top edge (row 0) → Bottom edge (row 29)
Bottom edge (row 29) → Top edge (row 0)
Algorithm Overview:
- Calculate column of previous and new position using
mod 40 - If columns sum to 39, horizontal wrap occurred
- Calculate row of new position using
div 40 - If row is 30 or -1, vertical wrap occurred
- Apply appropriate correction (+/- 1200 for vertical, +/- 40 for horizontal)
Design Note: The topology system is currently hardcoded for torus. Future improvements will make this configurable via function pointers.
Function: movePlayer()
Complete player movement pipeline:
- Check if UI is active (if so, skip all movement)
- Reset
moveBlockedflag to 0 - Save current position as
playerPrevPos - Check input (w/a/s/d) and call appropriate movement function
- Apply topology corrections via
mvTopology() - Behavior Check: Look up the character at new position in Props layer
- Use
BehaviorJumpDictto call the appropriate behavior function - If
moveBlockedflag is still 0, execute the move:- Call
MoveInMemory()to update layer - Mark old and new positions as changed
- Call
The behavior system implements game object logic using a jump table pattern.
BehaviorJumpDict: var #128 ; Array of 128 function pointersThis array maps ASCII character values to function addresses. When the player moves to a position, the character at that position is used as an index into this table to call the appropriate behavior.
Currently Defined Behaviors:
- ASCII 0 (null) →
DoNothing - ASCII 32 (space) →
DoNothing - ASCII 35 ('#', wall) →
BlockMovement - ASCII 64 ('@', box) →
checkPushMovement
Since the processor doesn't support indirect jumps natively, the code uses a clever pattern:
; Load function address into r7
mov r7, functionAddress
; Call through stack manipulation
call IndirectCall
; IndirectCall implementation:
IndirectCall:
push r7 ; Push address onto stack
rts ; Return pops address and jumps to itDoNothing() - No-op for empty space
BlockMovement() - Sets moveBlocked = 1 to prevent movement
checkPushMovement() - Complex box pushing logic:
- Check if the object is actually a box ('@')
- Calculate movement direction from position difference
- Apply same movement to box position
- Apply topology corrections to box's new position
- Recursively check the behavior at box's destination
- If destination allows movement, call
MoveInMemory()for the box - If destination blocks, set
moveBlocked = 1
This recursive approach allows pushing multiple boxes in a chain. When box A is pushed into box B, box B's behavior function is called, which can then push box B into box C, etc.
A sophisticated system for managing stacked, interactive user interface elements.
The UI system implements a stack-based modal dialog pattern. Multiple UI elements can be stacked, but only the topmost one receives input and is fully rendered.
Key Variables:
UIStack[20]- Stack of up to 20 UI element addressesUIStackPointer- Points to current top of stackISUIActive- Boolean flag (0 = game active, 1 = UI active)UICurentlySelectedElement- Index of highlighted interactible elementUIPreviousSelectedElement- Previous selection (for unhighlighting)uIHighlightColor- Color for highlighted UI elements (yellow, 64512)
Each UI element is defined as a 7-word structure:
UIElement: var #7
[0] Function address - Input handler for this element
[1] Start position - Top-left corner screen position
[2] End position - Bottom-right corner screen position
[3] Default color - Color for rendering
[4] RLE data address - Compressed visual representation
[5] Interactible count - Number of selectable elements
[6] Interactible list addr - Array of interactible elements
Interactible Element Structure (3 words each):
InteractibleElement: var #3
[0] Start position - Bounding box start
[1] End position - Bounding box end
[2] Function address - Called when activated (Enter key)
UICall(r0) - Opens a UI element
- Extracts start position, end position, and RLE data from UI structure
- Calls
UIDrawToBuffer()to decompress RLE into UI layer - Calls
SquareFinder()to mark rectangular region as changed - Renders entire UI buffer with
FullScreenUIPrint() - Pushes element address onto UI stack
- Sets
ISUIActive = 1 - Initializes selection indices to 0
UIClose() - Closes the topmost UI element
- Resets selection tracking variables
- Pops UI stack pointer
- If stack is now empty:
- Sets
ISUIActive = 0 - Marks UI region for redraw
- Calls
ScreenRenderChanged()to show game underneath
- Sets
- If stack still has elements:
- Redraws previous UI element
UIHandeler() - Dispatches input to active UI or game
Called every frame from main loop:
- If
ISUIActive = 1: Calls the active UI element's function - If
ISUIActive = 0: Checks if ESC was pressed to open pause menu
UIInteractibleElementComputeShift(r0) - Updates selection
Takes a shift value (±1):
- Saves current selection as previous
- Adds shift to current selection
- Handles wrap-around using modulo arithmetic
- Handles underflow (65535 wraps to max)
UIInteractibleElementHighLightRender(r0) - Visual feedback
- Finds currently selected interactible element
- Sets printing color to highlight color (Blue)
- Uses
SquareFinder()to mark bounding box as changed - Calls
ScreenRenderUIChanged()to redraw with highlight - Repeats process for previously selected element with default color
UISelectedInteractibleElementInteract(r0) - Activates selection
- Finds currently selected interactible element
- Extracts its function address
- Calls function through
IndirectCallpattern
UIDrawToBuffer(r0, r1) - Decompresses RLE into UI layer
- r0: Starting screen position
- r1: Address of RLE data
Uses RLE decoder with special behavior: zeros in the RLE do not overwrite existing UI buffer contents. This allows transparent overlays when stacking UI elements.
SquareFinder(r0, r1) - Marks rectangular regions
Given start and end positions, calculates all screen indices in the rectangle and adds them to the changed indices buffer. Uses nested loops over X and Y coordinates.
The main menu demonstrates the full UI system:
MainMenu: var #7
[0] #MainMenuFunction - Handles w/s for navigation, Enter for selection
[1] 0 - Start at top-left
[2] 1199 - End at bottom-right (full screen)
[3] 0 - White color
[4] #MainMenuRLE - ASCII art title + menu
[5] 2 - Two options
[6] #MainMenuInteractibleList
MainMenuInteractibleList:
[0] #NewGameButton - "NEW GAME" option
[1] #LevelSelectButton - "CHOOSE LEVEL" optionWhen "NEW GAME" is activated, its function calls UIClose(), loads a level via LoadStage(), and calls FullScreenPrint() to show the game.
Run-Length Encoding is used to compress level data and UI graphics, saving significant memory.
RLE data is stored as pairs of values terminated by 0:
[count₁, character₁, count₂, character₂, ..., 0]
Example:
"AAABBC" → [3, 'A', 2, 'B', 1, 'C', 0]
Compression Ratios:
- TestLevel: 1200 words → 187 words (84.4% saved)
- MainMenu: 1200 words → 285 words (76.2% saved)
RLEDecoder(r0, r1) - Decompresses RLE to memory
- r0: Destination address (where to write)
- r1: Source RLE data address
Algorithm:
- Read count and character from RLE
- Write character
counttimes to destination - Advance destination pointer
- Repeat until terminator (0) is reached
Used by LoadStage() to decompress all four layers.
RLEEncoder() - TODO: Not yet implemented
RLETraverser(r0, r1) - Random access into RLE (experimental)
This function provides efficient random access to compressed data without full decompression. It maintains a buffer (RLETraverserBuffer) of recent positions to optimize sequential access patterns.
Note: This function is defined but not currently used in the codebase.
Function: LoadStage(r2)
Takes a pointer to a Level structure and loads all layer data.
Level: var #5
[0] UI layer RLE address
[1] Props layer RLE address
[2] Background layer RLE address
[3] Behavior layer RLE address
[4] Topology identifier
Loading Process:
For each layer:
- Load destination pointer (e.g.,
currentUILayer) - Load RLE source address from level structure
- Call
RLEDecoder(destination, source) - Increment to next layer
Finally, load topology identifier (currently unused, always torus).
Current Levels:
TestLevel- Demo level with box pushingLevel1,Level2- TODO: Not yet defined
Function: MoveInMemory(r0, r1)
Moves a character from one position to another in the Props layer:
- r1 = source position
- r0 = destination position
- Read character at source
- Write space (' ') to source
- Write character to destination
Critical Note: Always call from last-moved object to first, otherwise objects can "Thanos snap" (disappear) if their destination overwrites their source before reading.
Instead of hardcoding layer addresses, the game uses pointer variables:
currentUILayercurrentPropLayercurrentBackgroundLayercurrentBehaviourLayer
This design allows easy level switching via LoadStage() without complex memory management.
string "text data"Null-terminated character array.
var #N
[count, char, count, char, ..., 0]
var #1200
1200-character screen buffer (40×30).
var #length
[element₀, element₁, ...]
var #7
[function, startPos, endPos, color, rleAddr, interactCount, interactListAddr]
var #3
[startPos, endPos, functionAddr]
var #5
[uiRLE, propsRLE, bgRLE, behaviorRLE, topology]
- Initialize
currentScreenIndexesChangedIndexpointer - Set up layer pointers to default memory regions
- Clear UI layer using RLE decoder
- Initialize UI stack pointer
- Position player at index 80
- Place player character 'A' in Props layer
- Call
UICall(#MainMenu)to show main menu
mainLoop:
InputHandler() - Read one character
movePlayer() - Process movement if game active
UIHandeler() - Dispatch to UI or check for ESC
render() - Draw changed screen positions
jmp mainLoop
- Menu Phase: Player navigates menu with W/S, presses Enter
- Menu Action: Selected function calls
UIClose()andLoadStage() - Game Phase: Player moves with W/A/S/D
- Movement updates Props layer
- Behavior system handles collisions and box pushing
- Dirty rendering updates only changed positions
- Pause: Player presses ESC
- Confirmation prompt is pushed onto UI stack
- Game continues underneath but receives no input
- Resume: Player selects "NO" on prompt
- UI closes, game continues from exact state
Only redraws changed portions of screen (typically 2-10 positions per frame vs. 1200).
Reduces memory usage by 75-85% for large levels and UI elements.
Renders layers from top to bottom, stopping at first non-space character. Avoids redundant drawing.
Enables polymorphic behavior without complex branching logic.
Render functions are pure - they read from layers but don't modify game state, preventing render/update coupling bugs.
-
Hardcoded Topology: Torus logic is embedded in
mvTopology(). Other manifolds require code changes. -
No Undo System: Cannot reverse moves (would require state history stack).
-
Limited Object Types: Only player, walls, and boxes. No buttons, doors, teleporters, etc.
-
Static Backgrounds: Background layer is static; no animated elements.
-
Single-Character Objects: All objects are single ASCII characters (no multi-tile sprites).
Short Term:
- Goal tiles ('P') to detect level completion
- Win condition checking and level progression
- More levels with increasing difficulty
Medium Term:
- Configurable topology system with function pointers
- Additional object types (buttons, doors, pressure plates)
- Undo function with state stack
- Level selection menu
Long Term:
- Non-orientable surfaces (Möbius strip, Klein bottle)
- Non-traditional 2D manifolds
- Animated backgrounds
- More sophisticated sprite system
The codebase follows these informal conventions:
- r0-r2: General purpose, frequently clobbered
- r3-r6: Local variables within functions
- r7: Special role for indirect calls (function pointer)
All functions preserve registers by pushing/popping around their logic.
Position Calculation:
; Convert (x, y) to linear index
; index = y * 40 + x
mul r1, r3, r2 ; r1 = y * 40
add r1, r1, r0 ; r1 = y * 40 + xCharacter Lookup:
; Get character at position r1 in layer
load r0, currentPropLayer
add r0, r0, r1 ; r0 = layer address + position
loadi r2, r0 ; r2 = characterColored Output:
; Output character r4 with color r3 at position r1
add r4, r2, r3 ; r4 = character + color
outchar r4, r1 ; Display at positionThe code includes several debug outputs (search for "DEBUG"):
- Position 0: Shows
ISUIActivevalue - Position 1: Shows last input character (ASCII value)
- Position 2: Shows
UICurentlySelectedElementindex
These can be enabled/disabled by commenting the respective code blocks.
Dirty Rendering: Only redrawing portions of the screen that have changed since last frame.
RLE (Run-Length Encoding): Compression technique that stores repeated values as (count, value) pairs.
Topology: In this context, the "shape" of the game world and how edges connect (e.g., torus wraps both edges).
2D Manifold: A mathematical surface that locally looks like a flat plane but may have global properties like wrapping or twisting.
Jump Table: An array of function pointers used to implement polymorphic dispatch.
Indirect Call: Calling a function through a pointer rather than a direct address.
Modal Dialog: A UI element that blocks interaction with elements beneath it until closed.
Stack-Based UI: UI system where elements are stacked like plates, and only the top element is active.