mirror of
https://github.com/soconnor0919/eceg431.git
synced 2025-12-11 06:34:43 -05:00
project09 - snake game functional!
This commit is contained in:
93
09/Snake/Food.jack
Normal file
93
09/Snake/Food.jack
Normal 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
18
09/Snake/Main.jack
Normal 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
56
09/Snake/Point.jack
Normal 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
66
09/Snake/README.md
Normal 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
54
09/Snake/Random.jack
Normal 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
187
09/Snake/Snake.jack
Normal 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
209
09/Snake/SnakeGame.jack
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user