diff --git a/09_Battle/java/Battle.java b/09_Battle/java/Battle.java index 627906d0..c0b27906 100644 --- a/09_Battle/java/Battle.java +++ b/09_Battle/java/Battle.java @@ -7,18 +7,26 @@ import java.util.Random; import java.util.function.Predicate; import java.text.NumberFormat; + +/* This class holds the game state and the game logic */ public class Battle { + + /* parameters of the game */ private int seaSize; private int[] sizes; private int[] counts; - + + /* The game setup - the ships and the sea */ private ArrayList ships; private Sea sea; - private int[] losses; - private int hits; - private int misses; + /* game state counts */ + private int[] losses; // how many of each type of ship have been sunk + private int hits; // how many hits the player has made + private int misses; // how many misses the player has made + // Names of ships of each size. The game as written has ships of size 3, 4 and 5 but + // can easily be modified. It makes no sense to have a ship of size zero though. private static String NAMES_BY_SIZE[] = { "error", "size1", @@ -27,10 +35,11 @@ public class Battle { "aircraft carrier", "size5" }; + // Entrypoint public static void main(String args[]) { - Battle game = new Battle(6, - new int[] { 2, 3, 4 }, - new int[] { 2, 2, 2 }); + Battle game = new Battle(6, // Sea is 6 x 6 tiles + new int[] { 2, 3, 4 }, // Ships are of sizes 2, 3, and 4 + new int[] { 2, 2, 2 }); // there are two ships of each size game.play(); } @@ -39,7 +48,7 @@ public class Battle { sizes = shipSizes; counts = shipCounts; - /* validate parameters */ + // validate parameters if (seaSize < 4) throw new RuntimeException("Sea Size " + seaSize + " invalid, must be at least 4"); for (int sz : sizes) { @@ -51,20 +60,25 @@ public class Battle { throw new RuntimeException("Ship counts must match"); } - sea = new Sea(seaSize); - ships = new ArrayList(); - losses = new int[counts.length]; + // Initialize game state + sea = new Sea(seaSize); // holds what ship if any occupies each tile + ships = new ArrayList(); // positions and states of all the ships + losses = new int[counts.length]; // how many ships of each type have been sunk + // Build up the list of all the ships int shipNumber = 1; for (int type = 0; type < counts.length; ++type) { for (int i = 0; i < counts[i]; ++i) { - ships.add(new Ship(shipNumber++, "Ship", sizes[type])); + ships.add(new Ship(shipNumber++, sizes[type])); } } + // When we put the ships in the sea, we put the biggest ones in first, or they might + // not fit ArrayList largestFirst = new ArrayList<>(ships); Collections.sort(largestFirst, Comparator.comparingInt((Ship ship) -> ship.size()).reversed()); + // place each ship into the sea for (Ship ship : largestFirst) { ship.placeRandom(sea); } @@ -79,11 +93,13 @@ public class Battle { System.out.println("Start game"); Input input = new Input(seaSize); try { - while (lost < ships.size()) { - if (! input.readCoordinates()) { + while (lost < ships.size()) { // the game continues while some ships remain unsunk + if (! input.readCoordinates()) { // ... unless there is no more input from the user return; } + // The computer thinks of the sea as a grid of rows, from top to bottom. + // However, the user will use X and Y coordinates, with Y going bottom to top int row = seaSize - input.y(); int col = input.x() - 1; @@ -104,6 +120,9 @@ public class Battle { ship.hit(col, row); ++hits; System.out.println("A direct hit on ship number " + ship.id()); + + // If a ship was hit, we need to know whether it was sunk. + // If so, tell the player and update our counts if (ship.isSunk()) { ++lost; System.out.println("And you sunk it. Hurrah for the good guys."); @@ -142,6 +161,8 @@ public class Battle { } } catch (IOException e) { + // This should not happen running from console, but java requires us to check for it + System.err.println("System error.\n" + e); } } } diff --git a/09_Battle/java/Ship.java b/09_Battle/java/Ship.java index e3fb4f44..23605e5c 100644 --- a/09_Battle/java/Ship.java +++ b/09_Battle/java/Ship.java @@ -4,34 +4,41 @@ import java.util.Comparator; import java.util.Random; import java.util.function.Predicate; +/** A single ship, with its position and where it has been hit */ class Ship { - public static final int ORIENT_E=0; - public static final int ORIENT_SE=1; - public static final int ORIENT_S=2; - public static final int ORIENT_SW=3; + // These are the four directions that ships can be in + public static final int ORIENT_E=0; // goes East from starting position + public static final int ORIENT_SE=1; // goes SouthEast from starting position + public static final int ORIENT_S=2; // goes South from starting position + public static final int ORIENT_SW=3; // goes SouthWest from starting position - private int id; - private int size; - private String type; - private boolean placed; - private boolean sunk; - private ArrayList hits; + private int id; // ship number + private int size; // how many tiles it occupies + private boolean placed; // whether this ship is in the sea yet + private boolean sunk; // whether this ship has been sunk + private ArrayList hits; // which tiles of the ship have been hit - private int startX; + private int startX; // starting position coordinates private int startY; - private int orientX; + private int orientX; // x and y deltas from each tile occupied to the next private int orientY; - public Ship(int i, String name, int sz) { - id = i; type = name; size = sz; + public Ship(int i, int sz) { + id = i; size = sz; sunk = false; placed = false; hits = new ArrayList<>(Collections.nCopies(size, false)); } + /** @returns the ship number */ public int id() { return id; } + /** @returns the ship size */ public int size() { return size; } + /* record the ship as having been hit at the given coordinates */ public void hit(int x, int y) { + // need to work out how many tiles from the ship's starting position the hit is at + // that can be worked out from the difference between the starting X coord and this one + // unless the ship runs N-S, in which case use the Y coord instead int offset; if (orientX != 0) { offset = (x - startX) / orientX; @@ -40,11 +47,13 @@ class Ship { } hits.set(offset, true); + // if every tile of the ship has been hit, the ship is sunk sunk = hits.stream().allMatch(Predicate.isEqual(true)); } public boolean isSunk() { return sunk; } + // whether the ship has already been hit at the given coordinates public boolean wasHit(int x, int y) { int offset; if (orientX != 0) { @@ -55,6 +64,9 @@ class Ship { return hits.get(offset); }; + // Place the ship in the sea. + // choose a random starting position, and a random direction + // if that doesn't fit, keep picking different positions and directions public void placeRandom(Sea s) { Random random = new Random(); for (int tries = 0 ; tries < 1000 ; ++tries) { @@ -68,6 +80,77 @@ class Ship { throw new RuntimeException("Could not place any more ships"); } + // Attempt to fit the ship into the sea, starting from a given position and + // in a given direction + // This is by far the most complicated part of the program. + // It will start at the position provided, and attempt to occupy tiles in the + // requested direction. If it does not fit, either because of the edge of the + // sea, or because of ships already in place, it will try to extend the ship + // in the opposite direction instead. If that is not possible, it fails. + public boolean place(Sea s, int x, int y, int orient) { + if (placed) { + throw new RuntimeException("Program error - placed ship " + id + " twice"); + } + switch(orient) { + case ORIENT_E: // east is increasing X coordinate + orientX = 1; orientY = 0; + break; + case ORIENT_SE: // southeast is increasing X and Y + orientX = 1; orientY = 1; + break; + case ORIENT_S: // south is increasing Y + orientX = 0; orientY = 1; + break; + case ORIENT_SW: // southwest is increasing Y but decreasing X + orientX = -1; orientY = 1; + break; + default: + throw new RuntimeException("Invalid orientation " + orient); + } + + if (!s.isEmpty(x, y)) return false; // starting position is occupied - placing fails + + startX = x; startY = y; + int tilesPlaced = 1; + int nextX = startX; + int nextY = startY; + while (tilesPlaced < size) { + if (extendShip(s, nextX, nextY, nextX + orientX, nextY + orientY)) { + // It is clear to extend the ship forwards + tilesPlaced += 1; + nextX = nextX + orientX; + nextY = nextY + orientY; + } else { + int backX = startX - orientX; + int backY = startY - orientY; + + if (extendShip(s, startX, startY, backX, backY)) { + // We can move the ship backwards, so it can be one tile longer + tilesPlaced +=1; + startX = backX; + startY = backY; + } else { + // Could not make it longer or move it backwards + return false; + } + } + } + + // Mark in the sea which tiles this ship occupies + for (int i = 0; i < size; ++i) { + int sx = startX + i * orientX; + int sy = startY + i * orientY; + s.set(sx, sy, id); + } + + placed = true; + return true; + } + + // Check whether a ship which already occupies the "from" coordinates, + // can also occupy the "to" coordinates. + // They must be within the sea area, empty, and not cause the ship to cross + // over another ship private boolean extendShip(Sea s, int fromX, int fromY, int toX, int toY) { if (!s.isEmpty(toX, toY)) return false; // no space if ((fromX == toX)||(fromY == toY)) return true; // horizontal or vertical @@ -84,59 +167,4 @@ class Ship { if ((corner1 == 0) || (corner1 != corner2)) return true; return false; } - - public boolean place(Sea s, int x, int y, int orient) { - if (placed) { - throw new RuntimeException("Program error - placed ship " + id + " twice"); - } - switch(orient) { - case ORIENT_E: - orientX = 1; orientY = 0; - break; - case ORIENT_SE: - orientX = 1; orientY = 1; - break; - case ORIENT_S: - orientX = 0; orientY = 1; - break; - case ORIENT_SW: - orientX = -1; orientY = 1; - break; - default: - throw new RuntimeException("Invalid orientation " + orient); - } - - if (!s.isEmpty(x, y)) return false; - startX = x; startY = y; - int tilesPlaced = 1; - int nextX = startX; - int nextY = startY; - while (tilesPlaced < size) { - if (extendShip(s, nextX, nextY, nextX + orientX, nextY + orientY)) { - tilesPlaced += 1; - nextX = nextX + orientX; - nextY = nextY + orientY; - } else { - int backX = startX - orientX; - int backY = startY - orientY; - - if (extendShip(s, startX, startY, backX, backY)) { - tilesPlaced +=1; - startX = backX; - startY = backY; - } else { - return false; - } - } - } - - for (int i = 0; i < size; ++i) { - int sx = startX + i * orientX; - int sy = startY + i * orientY; - s.set(sx, sy, id); - } - placed = true; - return true; - } } -