From 9c5d755f03f706138ac914f678f5c30e7905c447 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Sun, 9 Nov 2025 13:01:01 -0500 Subject: [PATCH] project09 - snake game functional! --- 09/Snake/Food.jack | 93 ++++++++++++++++++ 09/Snake/Main.jack | 18 ++++ 09/Snake/Point.jack | 56 +++++++++++ 09/Snake/README.md | 66 +++++++++++++ 09/Snake/Random.jack | 54 +++++++++++ 09/Snake/Snake.jack | 187 +++++++++++++++++++++++++++++++++++ 09/Snake/SnakeGame.jack | 209 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 683 insertions(+) create mode 100644 09/Snake/Food.jack create mode 100644 09/Snake/Main.jack create mode 100644 09/Snake/Point.jack create mode 100644 09/Snake/README.md create mode 100644 09/Snake/Random.jack create mode 100644 09/Snake/Snake.jack create mode 100644 09/Snake/SnakeGame.jack diff --git a/09/Snake/Food.jack b/09/Snake/Food.jack new file mode 100644 index 0000000..d2c3983 --- /dev/null +++ b/09/Snake/Food.jack @@ -0,0 +1,93 @@ +// Manages food placement and collection for snake game +// Handles random positioning and collision detection +class Food { + field Point position; + field int size; + field boolean active; + + // create food at random position + constructor Food new() { + let size = 8; + let active = false; + let position = Point.new(0, 0); + return this; + } + + // free memory + method void dispose() { + do position.dispose(); + do Memory.deAlloc(this); + return; + } + + // place food at random grid position + method void spawn() { + var int gridX, gridY; + + // generate random grid coordinates (8x8 pixel grid) + let gridX = Random.between(1, 58) * 8; + let gridY = Random.between(1, 28) * 8; + + do position.setX(gridX); + do position.setY(gridY); + let active = true; + do draw(); + return; + } + + // draw food on screen as outlined box + method void draw() { + if (active) { + do drawOutlinedBox(position.getX(), position.getY(), size); + } + return; + } + + // draw outlined box for food + method void drawOutlinedBox(int x, int y, int squareSize) { + do Screen.setColor(true); + // draw top and bottom borders + do Screen.drawRectangle(x, y, x + squareSize - 1, y); + do Screen.drawRectangle(x, y + squareSize - 1, x + squareSize - 1, y + squareSize - 1); + + // draw left and right borders + do Screen.drawRectangle(x, y, x, y + squareSize - 1); + do Screen.drawRectangle(x + squareSize - 1, y, x + squareSize - 1, y + squareSize - 1); + + return; + } + + // remove food from screen + method void erase() { + do Screen.setColor(false); + do Screen.drawRectangle(position.getX(), position.getY(), + position.getX() + size - 1, position.getY() + size - 1); + return; + } + + // check if snake head collides with food + method boolean checkCollision(Point snakeHead) { + if (~active) { + return false; + } + + // check if snake head overlaps food position + if ((snakeHead.getX() = position.getX()) & + (snakeHead.getY() = position.getY())) { + do erase(); + let active = false; + return true; + } + return false; + } + + // get current position + method Point getPosition() { + return position; + } + + // check if food is currently active + method boolean isActive() { + return active; + } +} diff --git a/09/Snake/Main.jack b/09/Snake/Main.jack new file mode 100644 index 0000000..90f118b --- /dev/null +++ b/09/Snake/Main.jack @@ -0,0 +1,18 @@ +// Snake Game - Entry Point +// A classic Snake implementation with growing mechanics, +// collision detection, scoring, and smooth gameplay +class Main { + // program entry point + function void main() { + var SnakeGame game; + + // create and start game + let game = SnakeGame.new(); + do game.run(); + + // cleanup when done + do game.dispose(); + + return; + } +} diff --git a/09/Snake/Point.jack b/09/Snake/Point.jack new file mode 100644 index 0000000..1c380d6 --- /dev/null +++ b/09/Snake/Point.jack @@ -0,0 +1,56 @@ +// Simple coordinate class for clean position handling +// Provides basic x,y operations for snake and food placement +class Point { + field int x, y; + + // create point at given coordinates + constructor Point new(int ax, int ay) { + let x = ax; + let y = ay; + return this; + } + + // free memory + method void dispose() { + do Memory.deAlloc(this); + return; + } + + // get x coordinate + method int getX() { + return x; + } + + // get y coordinate + method int getY() { + return y; + } + + // update coordinates + method void setX(int newX) { + let x = newX; + return; + } + + method void setY(int newY) { + let y = newY; + return; + } + + // check if this point equals another + method boolean equals(Point other) { + return (x = other.getX()) & (y = other.getY()); + } + + // move point by offset + method void move(int dx, int dy) { + let x = x + dx; + let y = y + dy; + return; + } + + // copy this point to new point + method Point copy() { + return Point.new(x, y); + } +} diff --git a/09/Snake/README.md b/09/Snake/README.md new file mode 100644 index 0000000..701d3a8 --- /dev/null +++ b/09/Snake/README.md @@ -0,0 +1,66 @@ +# Snake Game - Project 09 + +A classic Snake implementation in Jack for the nand2tetris course. Snake grows when eating food, speed increases with score, and game ends on collision. + +## How to Run + +```bash +# Compile Jack code to VM files +./nand2tetris/tools/JackCompiler.sh projects/09/Snake + +# Run VM Emulator (GUI will open) +./nand2tetris/tools/VMEmulator.sh +``` + +In VM Emulator: Load Program → Select Snake folder → Run + +## Controls + +- **Arrow Keys** - Move snake (up, down, left, right) +- **Space** - Pause/resume +- **Q** - Quit game +- **R** - Restart after game over + +## Game Files + +**`Main.jack`** - Program entry point, creates and runs game + +**`Point.jack`** - Simple x,y coordinate class for positions + +**`Random.jack`** - Linear Congruential Generator for food placement + +**`Food.jack`** - Food management and rendering +- Spawns at random grid-aligned positions +- Draws as outlined box for visibility +- Collision detection with snake head + +**`Snake.jack`** - Snake body, movement, and collision logic +- Array-based body with head/tail management +- Growth mechanics when eating food +- Wall and self-collision detection + +**`SnakeGame.jack`** - Main game loop and state management +- Input processing and game state +- Score tracking and UI display +- Game over and restart handling + +## Game Mechanics + +- **Movement**: 8×8 pixel grid-based motion +- **Growth**: Snake extends when eating food +- **Scoring**: +10 points per food, speed increases +- **Collision**: Game ends on wall hit or self-collision +- **Restart**: Play multiple rounds without program restart + +## Technical Notes + +- **Memory management**: All classes properly dispose resources +- **Efficient rendering**: Only redraws changed areas (head/tail) +- **Grid alignment**: Snake and food use 8×8 pixel squares +- **Clean architecture**: Modular design with separate responsibilities + +**Project 09 Requirements Met:** +- Interactive program with user input +- Graphical display and animation +- Modular design with focused classes +- Programming complexity and challenge diff --git a/09/Snake/Random.jack b/09/Snake/Random.jack new file mode 100644 index 0000000..4e70a65 --- /dev/null +++ b/09/Snake/Random.jack @@ -0,0 +1,54 @@ +/* +Random Number Generator +Original author: Taylor Wacker +Modified by: Connor McKay, Sean O'Connor +https://gist.github.com/greneholt/2212294 + +This is a pseudo random number generator that uses the +Linear Congruential Generator (LCG) to generate random +numbers. +*/ +class Random { + static int x; + + /* + Sets a new seed value. + */ + function void seed(int seed) { + let x = seed; + return; + } + + /* + Returns a mod b. b must be positive. + */ + function int mod(int a, int b) { + if (a < 0) { + let a = -a; + } + + while ((a + 1) > b) { + let a = a - b; + } + + return a; + } + + /* + Returns the next random number. Can be negative or positive. + */ + function int next() { + let x = 7919 + (17*x); + return x; + } + + /* + Returns a random value between x (inclusive) and y (non-inclusive). + y must be greater than x. + */ + function int between(int x, int y) { + var int diff; + let diff = y - x; + return Random.mod(Random.next(), diff) + x; + } +} diff --git a/09/Snake/Snake.jack b/09/Snake/Snake.jack new file mode 100644 index 0000000..89afd86 --- /dev/null +++ b/09/Snake/Snake.jack @@ -0,0 +1,187 @@ +// Manages snake body segments and movement mechanics +// Handles growth, collision detection, and rendering +class Snake { + field Array body; + field int length; + field int maxLength; + field int direction; + field int size; + field Point head; + + // create snake with initial length at center screen + constructor Snake new() { + let maxLength = 200; + let length = 3; + let size = 8; + let direction = 4; // right + let body = Array.new(maxLength); + + // initialize head at screen center + let head = Point.new(256, 128); + let body[0] = head; + + // create initial body segments + let body[1] = Point.new(248, 128); + let body[2] = Point.new(240, 128); + + do draw(); + return this; + } + + // free memory + method void dispose() { + var int i; + var Point segment; + + let i = 0; + while (i < length) { + let segment = body[i]; + do segment.dispose(); + let i = i + 1; + } + do body.dispose(); + do Memory.deAlloc(this); + return; + } + + // draw entire snake as solid squares + method void draw() { + var int i; + var Point segment; + + do Screen.setColor(true); + let i = 0; + while (i < length) { + let segment = body[i]; + do Screen.drawRectangle(segment.getX(), segment.getY(), + segment.getX() + size - 1, segment.getY() + size - 1); + let i = i + 1; + } + return; + } + + // erase tail segment + method void eraseTail() { + var Point tail; + let tail = body[length - 1]; + do Screen.setColor(false); + do Screen.drawRectangle(tail.getX(), tail.getY(), + tail.getX() + size, tail.getY() + size); + return; + } + + // move snake one step forward with optional growth + method void move(boolean grow) { + var int i; + var Point newHead; + var Point oldTail; + var int newX, newY; + + // calculate new head position based on direction + let newX = head.getX(); + let newY = head.getY(); + if (direction = 1) { let newY = newY - 8; } // up + if (direction = 2) { let newY = newY + 8; } // down + if (direction = 3) { let newX = newX - 8; } // left + if (direction = 4) { let newX = newX + 8; } // right + + // create new head first + let newHead = Point.new(newX, newY); + + if (grow) { + // growing: shift everything back and add new head + let i = length; + while (i > 0) { + let body[i] = body[i - 1]; + let i = i - 1; + } + let body[0] = newHead; + let head = newHead; + let length = length + 1; + + // draw new head only + do Screen.setColor(true); + do Screen.drawRectangle(head.getX(), head.getY(), + head.getX() + size - 1, head.getY() + size - 1); + } else { + // not growing: move tail to front as new head + let oldTail = body[length - 1]; + + // erase old tail first + do Screen.setColor(false); + do Screen.drawRectangle(oldTail.getX(), oldTail.getY(), + oldTail.getX() + size - 1, oldTail.getY() + size - 1); + + // shift body segments back + let i = length - 1; + while (i > 0) { + let body[i] = body[i - 1]; + let i = i - 1; + } + + // dispose old tail and use new head + do oldTail.dispose(); + let body[0] = newHead; + let head = newHead; + + // draw new head + do Screen.setColor(true); + do Screen.drawRectangle(head.getX(), head.getY(), + head.getX() + size - 1, head.getY() + size - 1); + } + + return; + } + + // change direction (prevent reverse moves) + method void setDirection(int newDirection) { + // prevent moving directly backwards + if (((direction = 1) & (newDirection = 2)) | // up -> down + ((direction = 2) & (newDirection = 1)) | // down -> up + ((direction = 3) & (newDirection = 4)) | // left -> right + ((direction = 4) & (newDirection = 3))) { // right -> left + return; + } + let direction = newDirection; + return; + } + + // check wall collision + method boolean hitWall() { + return (head.getX() < 8) | (head.getX() > 496) | + (head.getY() < 8) | (head.getY() > 240); + } + + // check self collision + method boolean hitSelf() { + var int i; + var Point segment; + + let i = 1; // skip head (index 0) + while (i < length) { + let segment = body[i]; + if (head.equals(segment)) { + return true; + } + let i = i + 1; + } + return false; + } + + // get head position + method Point getHead() { + return head; + } + + // get current length + method int getLength() { + return length; + } + + // get current direction + method int getDirection() { + return direction; + } + + +} diff --git a/09/Snake/SnakeGame.jack b/09/Snake/SnakeGame.jack new file mode 100644 index 0000000..20758ff --- /dev/null +++ b/09/Snake/SnakeGame.jack @@ -0,0 +1,209 @@ +// Main game controller for Snake +// Handles input, game loop, scoring, and game state management +class SnakeGame { + field Snake snake; + field Food food; + field int score; + field boolean gameOver; + field boolean paused; + field int speed; + + // create new snake game + constructor SnakeGame new() { + let snake = Snake.new(); + let food = Food.new(); + do food.spawn(); // spawn food at random position + let score = 0; + let gameOver = false; + let paused = false; + let speed = 150; // milliseconds between moves + + do Random.seed(123); // initialize random generator + do drawUI(); + return this; + } + + // free memory + method void dispose() { + do snake.dispose(); + do food.dispose(); + do Memory.deAlloc(this); + return; + } + + // draw game interface elements + method void drawUI() { + // draw thick border for clear game boundaries + do Screen.setColor(true); + do Screen.drawRectangle(0, 0, 511, 7); // top border + do Screen.drawRectangle(0, 248, 511, 255); // bottom border + do Screen.drawRectangle(0, 0, 7, 255); // left border + do Screen.drawRectangle(504, 0, 511, 255); // right border + + do showScore(); + do showInstructions(); + return; + } + + // display current score + method void showScore() { + do Output.moveCursor(0, 0); + do Output.printString("Score: "); + do Output.printInt(score); + return; + } + + // display game instructions + method void showInstructions() { + do Output.moveCursor(0, 20); + do Output.printString("Arrow Keys: Move Space: Pause Q: Quit"); + return; + } + + // display game over message + method void showGameOver() { + do Output.moveCursor(12, 20); + do Output.printString("GAME OVER!"); + do Output.moveCursor(14, 18); + do Output.printString("Final Score: "); + do Output.printInt(score); + do Output.moveCursor(16, 15); + do Output.printString("Press R to restart or Q to quit"); + return; + } + + // handle user input + method void processInput() { + var char key; + let key = Keyboard.keyPressed(); + + if (key = 81) { let gameOver = true; } // q - quit + if (key = 32) { let paused = ~paused; } // space - pause + if (key = 131) { do snake.setDirection(1); } // up arrow + if (key = 133) { do snake.setDirection(2); } // down arrow + if (key = 130) { do snake.setDirection(3); } // left arrow + if (key = 132) { do snake.setDirection(4); } // right arrow + + return; + } + + // update game state one frame + method void update() { + var boolean ateFood; + var Point nextHead, currentHead; + var int nextX, nextY, currentDirection; + + if (paused | gameOver) { + return; + } + + // get current head and direction + let currentHead = snake.getHead(); + let currentDirection = snake.getDirection(); + let nextX = currentHead.getX(); + let nextY = currentHead.getY(); + + // calculate next head position based on direction + if (currentDirection = 1) { let nextY = nextY - 8; } // up + if (currentDirection = 2) { let nextY = nextY + 8; } // down + if (currentDirection = 3) { let nextX = nextX - 8; } // left + if (currentDirection = 4) { let nextX = nextX + 8; } // right + + let nextHead = Point.new(nextX, nextY); + + // check food collision at next position + let ateFood = food.checkCollision(nextHead); + do nextHead.dispose(); + + // move snake (with or without growth) + do snake.move(ateFood); + + // check collisions after movement + if (snake.hitWall() | snake.hitSelf()) { + let gameOver = true; + return; + } + + // handle food consumption + if (ateFood) { + let score = score + 10; + do food.spawn(); + do showScore(); + + // increase speed slightly + if (speed > 80) { + let speed = speed - 2; + } + } + + return; + } + + // wait for game restart input + method boolean waitForRestart() { + var char key; + + while (true) { + let key = Keyboard.keyPressed(); + if (key = 82) { // r key + return true; + } + if (key = 81) { // q key + return false; + } + do Sys.wait(50); + } + return false; + } + + // main game loop + method void run() { + var boolean restart; + + while (true) { + // game running loop + while (~gameOver) { + do processInput(); + do update(); + do Sys.wait(speed); + } + + // game over screen + do showGameOver(); + let restart = waitForRestart(); + + if (restart) { + // reset game state + do Screen.clearScreen(); + do snake.dispose(); + do food.dispose(); + let snake = Snake.new(); + let food = Food.new(); + do food.spawn(); + let score = 0; + let gameOver = false; + let paused = false; + let speed = 150; + do drawUI(); + } else { + return; // quit game + } + } + return; + } + + // get current score + method int getScore() { + return score; + } + + // check if game is over + method boolean isGameOver() { + return gameOver; + } + + // check if game is paused + method boolean isPaused() { + return paused; + } +}