From 353e735515a0ef16b53010a6dab586e1bd3081f1 Mon Sep 17 00:00:00 2001 From: Hanna Lee Date: Mon, 3 Apr 2017 22:58:00 -0400 Subject: [PATCH 1/3] add space shooters plugin --- .../mit/d54/plugins/shooter/ShooterBoard.java | 171 +++++++++++++ .../d54/plugins/shooter/ShooterPlugin.java | 234 ++++++++++++++++++ 2 files changed, 405 insertions(+) create mode 100644 src/edu/mit/d54/plugins/shooter/ShooterBoard.java create mode 100644 src/edu/mit/d54/plugins/shooter/ShooterPlugin.java 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..e70690e --- /dev/null +++ b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java @@ -0,0 +1,171 @@ +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 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 _scale) { + width = _width; + height = _height; + scale = _scale; + ships = new int[width][height]; + rgb = new int[width * scale][height][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][k] = 0; + rgb[2*defender + 1][height - 1][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][k] = defenderColor[k]; + rgb[2*defender + 1][height - 1][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][k] = colors[val][k]; + rgb[2*i+1][j][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; 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..e3ab9e6 --- /dev/null +++ b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java @@ -0,0 +1,234 @@ +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 scoreColumn = 8; + 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 = .80; // Time between animations, decreases with level + private double lastShipSpawnTime; + private double newShipSpawnStep = 2.0; // 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 ArcadeController controller; + + private ShooterBoard board; + + // Pixels we are actually using. + private final int height = 15; + private final int width = 8; + private int[][][] rgb = new int[width][height][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, 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; + 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 { + Thread.sleep(1500); + } catch (InterruptedException e) { + System.out.println(e); + } + 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) { + for (int i = 0; i < width; i++) { + for (int j = 0; j < height; j++) { + display.setPixelRGB(i, j, rgb[i][j][0], rgb[i][j][1], rgb[i][j][2]); + } + } + drawScore(display); + } + } + + // Handle an arcade button event. + public void arcadeButton(byte b) { + switch (gameState) { + case IDLE: + case GAME_END: + // 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.startGame(); + gameState = State.GAME; + score = 0; + 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 = (.80 - .2 * level); + System.out.println("hit, score: " + score + " level: " + level); + } + break; + default: + break; + } + default: + break; + } + } + + private void drawScore(Display2D display) { + for (int i = 0; i < height; i++) { + if ((score & 1< 0) { + display.setPixelRGB(scoreColumn, i, scoreColor[0], scoreColor[1], scoreColor[2]); + } + } + } + +} From 1bdde123dc5da6e2bf544ff0319dae87f1f16409 Mon Sep 17 00:00:00 2001 From: Hanna Lee Date: Mon, 3 Apr 2017 23:57:19 -0400 Subject: [PATCH 2/3] modify to account for dead pixels --- .../mit/d54/plugins/shooter/ShooterBoard.java | 18 ++++++++++-------- .../mit/d54/plugins/shooter/ShooterPlugin.java | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/edu/mit/d54/plugins/shooter/ShooterBoard.java b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java index e70690e..9544c69 100644 --- a/src/edu/mit/d54/plugins/shooter/ShooterBoard.java +++ b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java @@ -15,6 +15,7 @@ public class ShooterBoard { 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} @@ -24,12 +25,13 @@ public class ShooterBoard { // to stay consistent with graphics conventions. private int[][][] rgb; - public ShooterBoard(int _width, int _height, int _scale) { + 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][3]; + rgb = new int[width * scale][height + verticalOffset][3]; int MAX_WIDTH = 9; int MAX_HEIGHT = 17; assert width * scale <= MAX_WIDTH; @@ -63,14 +65,14 @@ private int randInt(int n) { private void updateDefender(int col) { // Clear old squares. for (int k = 0; k < 3; k++) { - rgb[2*defender][height - 1][k] = 0; - rgb[2*defender + 1][height - 1][k] = 0; + 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][k] = defenderColor[k]; - rgb[2*defender + 1][height - 1][k] = defenderColor[k]; + rgb[2*defender][height - 1 + verticalOffset][k] = defenderColor[k]; + rgb[2*defender + 1][height - 1 + verticalOffset][k] = defenderColor[k]; } } @@ -79,8 +81,8 @@ 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][k] = colors[val][k]; - rgb[2*i+1][j][k] = colors[val][k]; + rgb[2*i][j + verticalOffset][k] = colors[val][k]; + rgb[2*i+1][j + verticalOffset][k] = colors[val][k]; } } diff --git a/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java index e3ab9e6..98d3863 100644 --- a/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java +++ b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java @@ -25,7 +25,7 @@ private enum State {IDLE, GAME, GAME_END}; private int score; private int[] scoreColor; private int level; - private final int scoreColumn = 8; + private final int scoreRow = 0; private final int levelDifference = 10; // 10 hits increases level // All time units are in seconds. @@ -48,9 +48,10 @@ private enum State {IDLE, GAME, GAME_END}; private ShooterBoard board; // Pixels we are actually using. - private final int height = 15; + 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][3]; + private int[][][] rgb = new int[width][height + verticalOffset][3]; public ShooterPlugin(Display2D display, double framerate) throws IOException { super(display, framerate); @@ -66,7 +67,7 @@ public ShooterPlugin(Display2D display, double framerate) throws IOException { gameState = State.IDLE; - board = new ShooterBoard(width, height, 2); + board = new ShooterBoard(width, height, verticalOffset, 2); scoreColor = new int[] {Color.orange.getRed(), Color.orange.getGreen(), Color.orange.getBlue()}; @@ -175,7 +176,7 @@ protected void loop() { } if (draw) { for (int i = 0; i < width; i++) { - for (int j = 0; j < height; j++) { + for (int j = 0; j < height + verticalOffset; j++) { display.setPixelRGB(i, j, rgb[i][j][0], rgb[i][j][1], rgb[i][j][2]); } } @@ -195,6 +196,7 @@ public void arcadeButton(byte b) { board.startGame(); gameState = State.GAME; score = 0; + level = 1; break; case GAME: // Moves defender or shoots. @@ -211,7 +213,7 @@ public void arcadeButton(byte b) { // TODO: trigger some sort of special animation? score += 1; level = (score / levelDifference) + 1; - animStep = (.80 - .2 * level); + animStep = (.80 - .1 * level); System.out.println("hit, score: " + score + " level: " + level); } break; @@ -224,9 +226,9 @@ public void arcadeButton(byte b) { } private void drawScore(Display2D display) { - for (int i = 0; i < height; i++) { + for (int i = 0; i < width; i++) { if ((score & 1< 0) { - display.setPixelRGB(scoreColumn, i, scoreColor[0], scoreColor[1], scoreColor[2]); + display.setPixelRGB(width - i, scoreRow, scoreColor[0], scoreColor[1], scoreColor[2]); } } } From 248cb97dadabe5ca7fd41d14b8e43b082be209d6 Mon Sep 17 00:00:00 2001 From: Hanna Lee Date: Fri, 7 Apr 2017 01:45:33 -0400 Subject: [PATCH 3/3] parameter tuning and tweaking display --- .../mit/d54/plugins/shooter/ShooterBoard.java | 2 +- .../d54/plugins/shooter/ShooterPlugin.java | 44 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/edu/mit/d54/plugins/shooter/ShooterBoard.java b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java index 9544c69..13f8ff5 100644 --- a/src/edu/mit/d54/plugins/shooter/ShooterBoard.java +++ b/src/edu/mit/d54/plugins/shooter/ShooterBoard.java @@ -98,7 +98,7 @@ public void endGame() { } } for (int i = 0; i < width * scale; i++) { - for (int j = 0; j < height; j++) { + for (int j = 0; j < height + verticalOffset; j++) { for (int k = 0; k < 3; k++) { rgb[i][j][k] = 0; } diff --git a/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java index 98d3863..b2a2a5d 100644 --- a/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java +++ b/src/edu/mit/d54/plugins/shooter/ShooterPlugin.java @@ -32,9 +32,9 @@ private enum State {IDLE, GAME, GAME_END}; private final double dt; private double time; private double lastAnimTime; - private double animStep = .80; // Time between animations, decreases with level + private double animStep = .75; // Time between animations, decreases with level private double lastShipSpawnTime; - private double newShipSpawnStep = 2.0; // Time between spawns + private double newShipSpawnStep = 1.8; // Time between spawns // For scrolling text display. private final String idleText = "P L A Y"; @@ -42,6 +42,7 @@ private enum State {IDLE, GAME, GAME_END}; 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; @@ -116,6 +117,7 @@ protected void loop() { 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; } @@ -155,10 +157,12 @@ protected void loop() { // 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; } @@ -175,28 +179,36 @@ protected void loop() { rgb = board.getColors(); } if (draw) { - 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); + drawGameState(display); } } // Handle an arcade button event. public void arcadeButton(byte b) { switch (gameState) { - case IDLE: 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(); - gameState = State.GAME; 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. @@ -213,7 +225,8 @@ public void arcadeButton(byte b) { // TODO: trigger some sort of special animation? score += 1; level = (score / levelDifference) + 1; - animStep = (.80 - .1 * level); + animStep = .75 * Math.pow(.92, level); + newShipSpawnStep = 1.8 * Math.pow(.92, level); System.out.println("hit, score: " + score + " level: " + level); } break; @@ -233,4 +246,13 @@ private void drawScore(Display2D display) { } } + 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); + } + }