diff --git a/src/edu/mit/d54/plugins/shooter/ShooterBoard.java b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java new file mode 100644 index 0000000..13f8ff5 --- /dev/null +++ b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java @@ -0,0 +1,173 @@ +package edu.mit.d54.plugins.shooter; + +import java.awt.Color; +import java.util.Random; +import java.util.Arrays; + +/** + * This class implements the board for the space invaders shooter game. + * It keeps track of the locations of the ships and the defender, and is + * able to return the correct colors to display to the shooter plugin. + */ +public class ShooterBoard { + // Map ship levels to colors. + private final int maxHitPoints = 6; + private int[][] colors = new int[maxHitPoints + 1][3]; + private final int width; + private final int height; + private final int verticalOffset; + private final int scale; // how wide each object is in pixels + private int[][] ships; // each entry is a possible ship location + private int defender; // column position in {0, 1, 2, 3} + private int[] defenderColor; + // Color map. + // First index increases left to right, second index increases top to bottom, + // to stay consistent with graphics conventions. + private int[][][] rgb; + + public ShooterBoard(int _width, int _height, int _verticalOffset, int _scale) { + width = _width; + height = _height; + verticalOffset = _verticalOffset; + scale = _scale; + ships = new int[width][height]; + rgb = new int[width * scale][height + verticalOffset][3]; + int MAX_WIDTH = 9; + int MAX_HEIGHT = 17; + assert width * scale <= MAX_WIDTH; + assert height <= MAX_HEIGHT; + initColors(); + } + + // Assign different colors to different difficulties of invading ships. + private void initColors() { + defenderColor = new int[] {0, 255, 0}; + // Zeroeth index must be black because empty square should be black. + Color[] COLORS = new Color[]{Color.black, Color.blue, Color.cyan, + Color.magenta, Color.pink, Color.red, + Color.yellow}; + assert COLORS.length >= colors.length; + for (int c = 0; c < colors.length; c++) { + colors[c][0] = COLORS[c].getRed(); + colors[c][1] = COLORS[c].getGreen(); + colors[c][2] = COLORS[c].getBlue(); + } + } + + private int randomColumn() { + return randInt(4); + } + + private int randInt(int n) { + return (int) (Math.random() * n); + } + + private void updateDefender(int col) { + // Clear old squares. + for (int k = 0; k < 3; k++) { + rgb[2*defender][height - 1 + verticalOffset][k] = 0; + rgb[2*defender + 1][height - 1 + verticalOffset][k] = 0; + } + // Update column and color in the correct entries in rgb. + defender = col; + for (int k = 0; k < 3; k++) { + rgb[2*defender][height - 1 + verticalOffset][k] = defenderColor[k]; + rgb[2*defender + 1][height - 1 + verticalOffset][k] = defenderColor[k]; + } + } + + // Updates this.ships and this.rgb to reflect the new value at ships[i][j]. + private void updateShip(int i, int j, int val) { + ships[i][j] = val; + // Update rgb. + for (int k = 0; k < 3; k++) { + rgb[2*i][j + verticalOffset][k] = colors[val][k]; + rgb[2*i+1][j + verticalOffset][k] = colors[val][k]; + } + } + + public void startGame() { + updateDefender(randomColumn()); + } + + public void endGame() { + // Clear ships and rgb. + for (int i = 0; i < width; i ++) { + for (int j = 0; j < height; j++) { + ships[i][j] = 0; + } + } + for (int i = 0; i < width * scale; i++) { + for (int j = 0; j < height + verticalOffset; j++) { + for (int k = 0; k < 3; k++) { + rgb[i][j][k] = 0; + } + } + } + } + + // Adds a ship in a random column. + // Always add to the top row. + public void addShip(int level) { + int col = randomColumn(); + // Hitpoints is number of shots it takes to kill this ship. + int hitpoints = Math.min(maxHitPoints, Math.max(1, randInt(level + 1))); + updateShip(col, 0, hitpoints); + } + + // Shifts the board down by one because one time step passed. + // Returns true if game over (if a ship reaches the last row). + public boolean shiftDown() { + boolean gameover = false; + // Start at bottom (highest y index) and shift. + for (int i = 0; i < 4; i++) { + for (int j = height - 2; j > -1; j--) { + if (ships[i][j] > 0) { + updateShip(i, j+1, ships[i][j]); + updateShip(i, j, 0); + } + } + if (ships[i][height - 1] > 0) { + gameover = true; + } + } + return gameover; + } + + // Shift defender left or right. + public void moveDefender(char b) { + switch (b) { + case 'L': + updateDefender(Math.max(0, defender - 1)); + break; + case 'R': + updateDefender(Math.min(3, defender + 1)); + break; + default: + System.out.println("impossible"); + break; + } + } + + // Shoot in a column. Returns true if a ship was hit and destroyed. + public boolean shoot() { + // Depending on where defender is, remove one hit point from the ship + // in that column that is closest to the bottom. + boolean hit = false; + for (int j = height - 1; j > -1; j--) { + if (ships[defender][j] > 0) { + updateShip(defender, j, ships[defender][j] - 1); + // Only counts as a hit if the ship was destroyed. + hit = (ships[defender][j] == 0); + break; + } + } + return hit; + } + + // Caller should not modify rgb, but I am too lazy to make a copy before + // returning it, so just trust myself not to mess it up. + public int[][][] getColors() { + return rgb; + } +} diff --git a/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java new file mode 100644 index 0000000..b2a2a5d --- /dev/null +++ b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java @@ -0,0 +1,258 @@ +package edu.mit.d54.plugins.shooter; + +import edu.mit.d54.ArcadeController; +import edu.mit.d54.ArcadeListener; +import edu.mit.d54.Display2D; +import edu.mit.d54.DisplayPlugin; +import edu.mit.d54.TwitterClient; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.io.IOException; + + +/** + * This plugin implements a space invaders shooter game. User input received + * over a TCP socket on port 12345. + */ + +public class ShooterPlugin extends DisplayPlugin implements ArcadeListener { + + private enum State {IDLE, GAME, GAME_END}; + private boolean beginEnd; // true when it's the first loop iteration in GAME_END + + private State gameState; + private int score; + private int[] scoreColor; + private int level; + private final int scoreRow = 0; + private final int levelDifference = 10; // 10 hits increases level + + // All time units are in seconds. + private final double dt; + private double time; + private double lastAnimTime; + private double animStep = .75; // Time between animations, decreases with level + private double lastShipSpawnTime; + private double newShipSpawnStep = 1.8; // Time between spawns + + // For scrolling text display. + private final String idleText = "P L A Y"; + private final String gameoverText = "SCORE: "; // Add their score + private int textPos; + private final double scrollStep = 0.05; // Time to slide characters to the left once + private double lastScrollTime; + private boolean donePrintingScore; + + private ArcadeController controller; + + private ShooterBoard board; + + // Pixels we are actually using. + private final int verticalOffset = 1; // Leave one row empty at the top. + private final int height = 14; + private final int width = 8; + private int[][][] rgb = new int[width][height + verticalOffset][3]; + + public ShooterPlugin(Display2D display, double framerate) throws IOException { + super(display, framerate); + dt = 1.0 / framerate; + time = 0.0; + lastAnimTime = 0.0; + lastShipSpawnTime = 0.0; + textPos = -10; // Starting position for text. + lastScrollTime = 0.0; + level = 1; + + controller = ArcadeController.getInstance(); + + gameState = State.IDLE; + + board = new ShooterBoard(width, height, verticalOffset, 2); + + scoreColor = new int[] {Color.orange.getRed(), Color.orange.getGreen(), Color.orange.getBlue()}; + + System.out.println("Game paused until client connect"); + TwitterClient.tweet("Space invaders is now being played on the MIT Green Building! #mittetris"); + } + + @Override + protected void onStart() { + controller.setListener(this); + } + + @Override + protected void loop() { + Display2D display = getDisplay(); // For board manipulation. + Graphics2D gr = display.getGraphics(); // For easy text drawing. + time += dt; + boolean update = false; + boolean draw = false; + + // Game logic, based on current game state. + switch (gameState) { + case IDLE: + // Draw text on the building and just wait. + if (time - lastScrollTime > scrollStep) { + gr.drawString(idleText, -textPos, 12); + textPos++; + // Reset after we show the whole string and wait a bit. + if (textPos > 5*idleText.length() + 1.0/scrollStep) { + textPos = -10; + } + lastScrollTime = time; + } + break; + case GAME: + // Update the game time, decide if ships need to animate. + // And we might need to add a new ship. + // Update board state when events happen. + // Handle gameover. + draw = true; + if (time - lastAnimTime > animStep) { + update = true; + lastAnimTime = time; + boolean gameover = board.shiftDown(); + if (gameover) { + System.out.println("game over, score was " + score); + TwitterClient.tweet("Someone scored " + score + " playing space invaders on the MIT Green Building! #mittetris"); + beginEnd = true; + donePrintingScore = false; + gameState = State.GAME_END; + break; + } + } + if (time - lastShipSpawnTime > newShipSpawnStep) { + update = true; + System.out.println("spawn"); + lastShipSpawnTime = time; + board.addShip(level); + } + break; + case GAME_END: + // If we just ended the game, clear up some loose ends like actually + // ending the game on the board, and sleeping so that the player sees + // the end state of the game for a couple seconds. + if (beginEnd) { + // Do this here and not before so that screen is paused with + // ships still there, and then the screen clears after the delay below. + board.endGame(); + draw = true; + beginEnd = false; + // Pause for 2.5 seconds so player can see what the board looks like, + // then go into GAME_END. + try { + Thread.sleep(2500); + } catch (InterruptedException e) { + System.out.println(e); + break; + } + } + // Display some sort of end game message for a while + // and then revert to idle state. + String text = gameoverText + score; + if (time - lastScrollTime > scrollStep) { + gr.drawString(text, -textPos, 12); + textPos++; + // Reset after we show the whole string and wait a bit. + if (textPos > 5*text.length() + 8) { + try { + System.out.println("done printing score"); + Thread.sleep(1500); + } catch (InterruptedException e) { + System.out.println(e); + } + donePrintingScore = true; + textPos = -10; + gameState = State.IDLE; + } + lastScrollTime = time; + } + break; + default: + break; + } + + // Display the current state of the game. + // Only update rgb if there was an actual change in pixels. + if (update) { + rgb = board.getColors(); + } + if (draw) { + drawGameState(display); + } + } + + // Handle an arcade button event. + public void arcadeButton(byte b) { + switch (gameState) { + case GAME_END: + if (!donePrintingScore) { + break; + } + case IDLE: + // Starts game. + TwitterClient.tweet("Beginning a game of space invaders on the MIT Green Building! #mittetris"); + textPos = -10; // Reset for next time we draw text + System.out.println("new game starting"); + board.endGame(); + board.startGame(); + score = 0; + level = 1; + rgb = board.getColors(); + // Clear entire screen. + // Need to clear top and bottom row separately. + Display2D display = getDisplay(); + for (int i = 0; i < width; i++) { + // display.setPixelRGB(i, 0, 0, 0, 0); + display.setPixelRGB(i, height - 1 + verticalOffset, 0, 0, 0); + } + drawGameState(display); + gameState = State.GAME; + break; + case GAME: + // Moves defender or shoots. + switch (b) { + case 'L': + board.moveDefender('L'); + break; + case 'R': + board.moveDefender('R'); + break; + case 'U': + boolean hit = board.shoot(); + if (hit) { + // TODO: trigger some sort of special animation? + score += 1; + level = (score / levelDifference) + 1; + animStep = .75 * Math.pow(.92, level); + newShipSpawnStep = 1.8 * Math.pow(.92, level); + System.out.println("hit, score: " + score + " level: " + level); + } + break; + default: + break; + } + default: + break; + } + } + + private void drawScore(Display2D display) { + for (int i = 0; i < width; i++) { + if ((score & 1< 0) { + display.setPixelRGB(width - i, scoreRow, scoreColor[0], scoreColor[1], scoreColor[2]); + } + } + } + + private void drawGameState(Display2D display) { + for (int i = 0; i < width; i++) { + for (int j = 0; j < height + verticalOffset; j++) { + display.setPixelRGB(i, j, rgb[i][j][0], rgb[i][j][1], rgb[i][j][2]); + } + } + drawScore(display); + } + +}