project09 - snake game functional!

This commit is contained in:
2025-11-09 13:01:01 -05:00
parent 59f9433b2b
commit 9c5d755f03
7 changed files with 683 additions and 0 deletions

93
09/Snake/Food.jack Normal file
View File

@@ -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;
}
}

18
09/Snake/Main.jack Normal file
View File

@@ -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;
}
}

56
09/Snake/Point.jack Normal file
View File

@@ -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);
}
}

66
09/Snake/README.md Normal file
View File

@@ -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

54
09/Snake/Random.jack Normal file
View File

@@ -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;
}
}

187
09/Snake/Snake.jack Normal file
View File

@@ -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;
}
}

209
09/Snake/SnakeGame.jack Normal file
View File

@@ -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;
}
}