From e2d2cfcc8449cba7fe86c7ba44238d5d0fc8d817 Mon Sep 17 00:00:00 2001 From: Josh Gribbon Date: Sun, 2 Jan 2022 20:55:24 -0500 Subject: [PATCH 01/13] Add script to find missing implementations --- find-missing-implementations.js | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 find-missing-implementations.js diff --git a/find-missing-implementations.js b/find-missing-implementations.js new file mode 100644 index 00000000..04cf3f84 --- /dev/null +++ b/find-missing-implementations.js @@ -0,0 +1,85 @@ +/** + * Program to find games that are missing solutions in a given language + * + * Scan each game folder, check for a folder for each language, and also make + * sure there's at least one file of the expected extension and not just a + * readme or something + */ + +const fs = require("fs"); +const glob = require("glob"); + +// relative path to the repository root +const ROOT_PATH = "."; + +const languages = [ + { name: "csharp", extension: "cs" }, + { name: "java", extension: "java" }, + { name: "javascript", extension: "js" }, + { name: "pascal", extension: "pas" }, + { name: "perl", extension: "pl" }, + { name: "python", extension: "py" }, + { name: "ruby", extension: "rb" }, + { name: "vbnet", extension: "vb" }, +]; + +const getFilesRecursive = async (path, extension) => { + return new Promise((resolve, reject) => { + glob(`${path}/**/*.${extension}`, (err, matches) => { + if (err) { + reject(err); + } + resolve(matches); + }); + }); +}; + +const getPuzzleFolders = () => { + return fs + .readdirSync(ROOT_PATH, { withFileTypes: true }) + .filter((dirEntry) => dirEntry.isDirectory()) + .filter((dirEntry) => ![".git", "node_modules"].includes(dirEntry.name)) + .map((dirEntry) => dirEntry.name); +}; + +(async () => { + let missingGames = {}; + let missingLanguageCounts = {}; + const puzzles = getPuzzleFolders(); + for (const puzzle of puzzles) { + for (const { name: language, extension } of languages) { + const files = await getFilesRecursive( + `${ROOT_PATH}/${puzzle}/${language}`, + extension + ); + if (files.length === 0) { + if (!missingGames[puzzle]) { + missingGames[puzzle] = []; + } + if (!missingLanguageCounts[language]) { + missingLanguageCounts[language] = 0; + } + missingGames[puzzle].push(language); + missingLanguageCounts[language]++; + } + } + } + const missingCount = Object.values(missingGames).flat().length; + if (missingCount === 0) { + console.log("All games have solutions for all languages"); + } else { + console.log(`Missing ${missingCount} implementations:`); + + console.log(`\nMissing languages by game:`); + for (const [puzzle, languages] of Object.entries(missingGames)) { + console.log(`${puzzle}: ${languages.join(", ")}`); + } + + console.log(`\nBy language:`); + for (const [language, count] of Object.entries(missingLanguageCounts)) { + console.log(`${language}: ${count} missing`); + } + } +})(); + +return; From 5d00a092af55935b604d4c7f00af71314420094c Mon Sep 17 00:00:00 2001 From: Paul Holt Date: Tue, 11 Jan 2022 00:00:22 +1100 Subject: [PATCH 02/13] Building kotlin files. IntelliJ import instructions. --- HOW_TO_RUN_THE_GAMES.md | 28 ++++++++++++++++++++-------- buildJvm/README.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/HOW_TO_RUN_THE_GAMES.md b/HOW_TO_RUN_THE_GAMES.md index 6df23205..f067f872 100644 --- a/HOW_TO_RUN_THE_GAMES.md +++ b/HOW_TO_RUN_THE_GAMES.md @@ -23,16 +23,23 @@ Alternatively, for non-dotnet compatible translations, you will need [Visual Stu ## java +{% tip %} + +You can build all the java and kotlin games at once +using the instructions in the [buildJvm directory](buildJvm/README.md) + +{% endtip %} + The Java translations can be run via the command line or from an IDE such as [Eclipse](https://www.eclipse.org/downloads/packages/release/kepler/sr1/eclipse-ide-java-developers) or [IntelliJ](https://www.jetbrains.com/idea/) To run from the command line, you will need a Java SDK (eg. [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) or [Open JDK](https://openjdk.java.net/)). 1. Navigate to the corresponding directory. 1. Compile the program with `javac`: - * eg. `javac AceyDuceyGame.java` + * eg. `javac AceyDuceyGame.java` 1. Run the compiled program with `java`: - * eg. `java AceyDuceyGame` - + * eg. `java AceyDuceyGame` + or if you are **using JDK11 or later** you can now execute a self contained java file that has a main method directly with `java .java`. ## javascript @@ -41,6 +48,11 @@ The javascript examples can be run from within your web browser: 1. Simply open the corresponding `.html` file from your web browser. +## kotlin + +Use the directions in [buildJvm](buildJvm/README.md) to build for kotlin. You can also use those directions to +build java games. + ## pascal The pascal examples can be run using [Free Pascal](https://www.freepascal.org/). Additionally, `.lsi` project files can be opened with the [Lazarus Project IDE](https://www.lazarus-ide.org/). @@ -48,7 +60,7 @@ The pascal examples can be run using [Free Pascal](https://www.freepascal.org/). The pascal examples include both *simple* (single-file) and *object-oriented* (in the `/object-pascal`directories) examples. 1. You can compile the program from the command line with the `fpc` command. - * eg. `fpc amazing.pas` + * eg. `fpc amazing.pas` 1. The output is an executable file that can be run directly. ## perl @@ -57,7 +69,7 @@ The perl translations can be run using a perl interpreter (a copy can be downloa 1. From the command-line, navigate to the corresponding directory. 1. Invoke with the `perl` command. - * eg. `perl aceyducey.pl` + * eg. `perl aceyducey.pl` ## python @@ -65,8 +77,8 @@ The python translations can be run from the command line by using the `py` inter 1. From the command-line, navigate to the corresponding directory. 1. Invoke with the `py` or `python` interpreter (depending on your python version). - * eg. `py acey_ducey_oo.py` - * eg. `python aceyducey.py` + * eg. `py acey_ducey_oo.py` + * eg. `python aceyducey.py` **Note** @@ -80,7 +92,7 @@ If you don't already have a ruby interpreter, you can download it from the [ruby 1. From the command-line, navigate to the corresponding directory. 1. Invoke with the `ruby` tool. - * eg. `ruby aceyducey.rb` + * eg. `ruby aceyducey.rb` ## vbnet diff --git a/buildJvm/README.md b/buildJvm/README.md index f0046ab3..c70bf573 100644 --- a/buildJvm/README.md +++ b/buildJvm/README.md @@ -20,6 +20,34 @@ Then, run a game You will need to install openjdk 17, because some games use advanced Java features. We should be using version 17 anyway, because anything less than 17 is deprecated. +--- +## Using an IDE to work on JVM games + +You can open the entire Basic Computer Games project in an IDE, with any IDE capable +of importing from a gradle project. + +### IntelliJ / Android Studio + +1. (Optional) If you want to make changes, or contribute a new kotlin or java version +of one of the games, use [github "fork"](https://docs.github.com/en/get-started/quickstart/fork-a-repo) +to create your own editable fork of the project. +2. Check out the code using `File` -> `New` -> `Project from Version Control` + 1. Enter the URL of the project. For the main project this will be `https://github.com/coding-horror/basic-computer-games.git`, for your +own fork this will be `https://github.com/YOURNAMEHERE/basic-computer-games.git` + 2. Choose a directory for the clone to live in +3. Click `Clone` + +The project will open, and eventually you will get a little alert box in the bottom right corner saying "Gradle build script found". + +Click the "Load" link in the alert box, to load the gradle project. + +You should see all the games appear on the left side of the screen. If you have loaded +your own fork, you can modify, commit and push your changes to github. + +If you are using the main `coding-horror` branch, you can still make and run your own changes. If +your git skills are up to the task, you might even fork the project and change your +local clone to point to your new forked project. + --- ## Adding a new game @@ -37,6 +65,7 @@ The `build.gradle` file will normally be identical to this: plugins { id 'application' + // id 'org.jetbrains.kotlin.jvm' // UNCOMMENT for kotlin projects } sourceSets { From 25c79621a0c42a3f755a962bb9a5913ef5ae5cf8 Mon Sep 17 00:00:00 2001 From: Paul Holt Date: Tue, 11 Jan 2022 00:09:09 +1100 Subject: [PATCH 03/13] Update HOW_TO_RUN_THE_GAMES.md remove bad formatting --- HOW_TO_RUN_THE_GAMES.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/HOW_TO_RUN_THE_GAMES.md b/HOW_TO_RUN_THE_GAMES.md index f067f872..fae0656f 100644 --- a/HOW_TO_RUN_THE_GAMES.md +++ b/HOW_TO_RUN_THE_GAMES.md @@ -23,13 +23,9 @@ Alternatively, for non-dotnet compatible translations, you will need [Visual Stu ## java -{% tip %} - -You can build all the java and kotlin games at once +**TIP:** You can build all the java and kotlin games at once using the instructions in the [buildJvm directory](buildJvm/README.md) -{% endtip %} - The Java translations can be run via the command line or from an IDE such as [Eclipse](https://www.eclipse.org/downloads/packages/release/kepler/sr1/eclipse-ide-java-developers) or [IntelliJ](https://www.jetbrains.com/idea/) To run from the command line, you will need a Java SDK (eg. [Oracle JDK](https://www.oracle.com/java/technologies/downloads/) or [Open JDK](https://openjdk.java.net/)). From 8b2c72bc21fc93fcabdb108721fbadad8ec0e1d3 Mon Sep 17 00:00:00 2001 From: Paul Holt Date: Tue, 11 Jan 2022 00:12:21 +1100 Subject: [PATCH 04/13] update README.md --- buildJvm/README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/buildJvm/README.md b/buildJvm/README.md index c70bf573..a5169abd 100644 --- a/buildJvm/README.md +++ b/buildJvm/README.md @@ -1,25 +1,28 @@ # JVM gradle scripts ## Quickstart +You will need to install openjdk 17, because some games use advanced Java features. +We should be using version 17 anyway, because anything less than 17 is deprecated. Build all the games: - - cd buildJvm - ./gradlew -q assemble installDist distributeBin distributeLib +```shell + cd buildJvm + ./gradlew -q assemble installDist distributeBin distributeLib +``` Then, run a game ### Mac or linux: - - build/distrib/bin/build_53_King_kotlin - +```shell +build/distrib/bin/build_53_King_kotlin +``` ### Windows [not tested yet] - build\distrib\bin\build_53_King_kotlin.bat +```shell +build\distrib\bin\build_53_King_kotlin.bat +``` -You will need to install openjdk 17, because some games use advanced Java features. -We should be using version 17 anyway, because anything less than 17 is deprecated. --- ## Using an IDE to work on JVM games @@ -62,7 +65,7 @@ there is some special requirement. directory for the java or kotlin file, and the class that contains the `main` method. The `build.gradle` file will normally be identical to this: - +```groovy plugins { id 'application' // id 'org.jetbrains.kotlin.jvm' // UNCOMMENT for kotlin projects @@ -83,6 +86,7 @@ The `build.gradle` file will normally be identical to this: application { mainClass = gameMain } +``` And the `gradle.properties` file should look like this: From 420597dbcf4ee0b1f7859b719ca2a46c53497326 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 19:21:15 +0000 Subject: [PATCH 05/13] First cut java battleships --- 09_Battle/java/Battle.java | 372 +++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 09_Battle/java/Battle.java diff --git a/09_Battle/java/Battle.java b/09_Battle/java/Battle.java new file mode 100644 index 00000000..aa53896c --- /dev/null +++ b/09_Battle/java/Battle.java @@ -0,0 +1,372 @@ +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Random; +import java.util.function.Predicate; +import java.text.NumberFormat; + +public class Battle { + private int seaSize; + private int[] sizes; + private int[] counts; + + private ArrayList ships; + private Sea sea; + + private int[] losses; + private int hits; + private int misses; + + private static String NAMES_BY_SIZE[] = { + "error", + "size1", + "destroyer", + "cruiser", + "aircraft carrier", + "size5" }; + + public static void main(String args[]) { + Battle game = new Battle(6, + new int[] { 2, 3, 4 }, + new int[] { 2, 2, 2 }); + game.play(); + } + + public Battle(int scale, int[] shipSizes, int[] shipCounts) { + seaSize = scale; + sizes = shipSizes; + counts = shipCounts; + + /* validate parameters */ + if (seaSize < 4) throw new RuntimeException("Sea Size " + seaSize + " invalid, must be at least 4"); + + for (int sz : sizes) { + if ((sz < 1) || (sz > seaSize)) + throw new RuntimeException("Ship has invalid size " + sz); + } + + if (counts.length != sizes.length) { + throw new RuntimeException("Ship counts must match"); + } + + sea = new Sea(seaSize); + ships = new ArrayList(); + losses = new int[counts.length]; + + 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])); + } + } + + ArrayList largestFirst = new ArrayList<>(ships); + Collections.sort(largestFirst, Comparator.comparingInt((Ship ship) -> ship.size()).reversed()); + + for (Ship ship : largestFirst) { + ship.placeRandom(sea); + } + } + + public void play() { + System.out.println("The following code of the bad guys' fleet disposition\nhas been captured but not decoded:\n"); + System.out.println(sea.encodedDump()); + System.out.println("De-code it and use it if you can\nbut keep the de-coding method a secret.\n"); + + int lost = 0; + System.out.println("Start game"); + try { + BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); + NumberFormat parser = NumberFormat.getIntegerInstance(); + + while (lost < ships.size()) { + System.out.print("\nTarget x,y\n> "); + String inputLine = input.readLine(); + if (inputLine == null) { + System.out.println("Game quit\n"); + return; + } + String[] coords = inputLine.split(","); + if (coords.length != 2) { + System.out.println("Need two coordinates separated by ','"); + continue; + } + int[] xy = new int[2]; + boolean error = false; + try { + for (int c = 0 ; c < 2; ++c ) { + int val = Integer.parseInt(coords[c].strip()); + if ((val < 1) || (val > seaSize)) { + System.out.println("Coordinates must be from 1 to " + seaSize); + error = true; + } else { + xy[c] = val; + } + } + } + catch (NumberFormatException ne) { + System.out.println("Coordinates must be numbers"); + error = true; + } + if (error) continue; + + int row = seaSize - xy[1]; + int col = xy[0] - 1; + + if (sea.isEmpty(col, row)) { + ++misses; + System.out.println("Splash! Try again."); + } else { + Ship ship = ships.get(sea.get(col, row) - 1); + if (ship.isSunk()) { + ++misses; + System.out.println("There used to be a ship at that point, but you sunk it."); + System.out.println("Splash! Try again."); + } else if (ship.wasHit(col, row)) { + ++misses; + System.out.println("You already put a hole in ship number " + ship.id()); + System.out.println("Splash! Try again."); + } else { + ship.hit(col, row); + ++hits; + System.out.println("A direct hit on ship number " + ship.id()); + if (ship.isSunk()) { + ++lost; + System.out.println("And you sunk it. Hurrah for the good guys."); + System.out.print("So far, the bad guys have lost "); + ArrayList typeDescription = new ArrayList<>(); + for (int i = 0 ; i < sizes.length; ++i) { + if (sizes[i] == ship.size()) { + ++losses[i]; + } + StringBuilder sb = new StringBuilder(); + sb.append(losses[i]); + sb.append(" "); + sb.append(NAMES_BY_SIZE[sizes[i]]); + if (losses[i] != 1) + sb.append("s"); + typeDescription.add(sb.toString()); + } + System.out.println(String.join(", ", typeDescription)); + double ratioNum = ((double)misses)/hits; + String ratio = NumberFormat.getInstance().format(ratioNum); + System.out.println("Your current splash/hit ratio is " + ratio); + + if (lost == ships.size()) { + System.out.println("You have totally wiped out the bad guys' fleet"); + System.out.println("With a final splash/hit ratio of " + ratio); + + if (misses == 0) { + System.out.println("Congratulations - A direct hit every time."); + } + + System.out.println("\n****************************\n"); + } + } + } + } + } + } + catch (IOException e) { + } + } + + private static 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; + + private int id; + private int size; + private String type; + private boolean placed; + private boolean sunk; + private ArrayList hits; + + private int startX; + private int startY; + private int orientX; + private int orientY; + + public Ship(int i, String name, int sz) { + id = i; type = name; size = sz; + sunk = false; placed = false; + hits = new ArrayList<>(Collections.nCopies(size, false)); + } + + public int id() { return id; } + public int size() { return size; } + + public void hit(int x, int y) { + int offset; + if (orientX != 0) { + offset = (x - startX) / orientX; + } else { + offset = (y - startY) / orientY; + } + hits.set(offset, true); + + sunk = hits.stream().allMatch(Predicate.isEqual(true)); + } + + public boolean isSunk() { return sunk; } + + public boolean wasHit(int x, int y) { + int offset; + if (orientX != 0) { + offset = (x - startX) / orientX; + } else { + offset = (y - startY) / orientY; + } + return hits.get(offset); + }; + + public void placeRandom(Sea s) { + Random random = new Random(); + for (int tries = 0 ; tries < 1000 ; ++tries) { + int x = random.nextInt(s.size()); + int y = random.nextInt(s.size()); + int orient = random.nextInt(4); + + if (place(s, x, y, orient)) return; + } + + throw new RuntimeException("Could not place any more ships"); + } + + 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 + + // we can extend the ship without colliding, but we are going diagonally + // and it should not be possible for two ships to cross each other on + // opposite diagonals. + + // check the two tiles that would cross us here - if either is empty, we are OK + // if they both contain different ships, we are OK + // but if they both contain the same ship, we are crossing! + int corner1 = s.get(fromX, toY); + int corner2 = s.get(toX, fromY); + 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; + } + + } + + private static class Sea { + private int tiles[]; + private boolean hits[]; + + private int size; + public Sea(int make_size) { + size = make_size; + tiles = new int[size*size]; + } + + public int size() { return size; } + + public String encodedDump() { + StringBuilder out = new StringBuilder(); + for (int x = 0; x < size; ++x) { + for (int y = 0; y < size; ++y) + out.append(Integer.toString(get(x, y))); + out.append('\n'); + } + return out.toString(); + } + + /* return true if x,y is in the sea and empty + * return false if x,y is occupied or is out of range + */ + public boolean isEmpty(int x, int y) { + if ((x<0)||(x>=size)||(y<0)||(y>=size)) return false; + return (get(x,y) == 0); + } + + /* return the ship number, or zero if no ship */ + public int get(int x, int y) { + return tiles[index(x,y)]; + } + + public void set(int x, int y, int value) { + tiles[index(x, y)] = value; + } + + public int shipHit(int x, int y) { + if (hits[index(x,y)]) return get(x, y); + else return 0; + } + + public void recordHit(int x, int y) { + hits[index(x, y)] = true; + } + + private int index(int x, int y) { + if ((x < 0) || (x >= size)) + throw new ArrayIndexOutOfBoundsException("Program error: x cannot be " + x); + if ((y < 0) || (y >= size)) + throw new ArrayIndexOutOfBoundsException("Program error: y cannot be " + y); + + return y*size + x; + } + } +} From c074beaf42e0c0aeb91413c152a827ac48942417 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 19:38:23 +0000 Subject: [PATCH 06/13] pull out input parsing --- 09_Battle/java/Battle.java | 38 ++++---------------------------------- 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/09_Battle/java/Battle.java b/09_Battle/java/Battle.java index aa53896c..b548292c 100644 --- a/09_Battle/java/Battle.java +++ b/09_Battle/java/Battle.java @@ -1,5 +1,3 @@ -import java.io.BufferedReader; -import java.io.InputStreamReader; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -79,43 +77,15 @@ public class Battle { int lost = 0; System.out.println("Start game"); + Input input = new Input(seaSize); try { - BufferedReader input = new BufferedReader(new InputStreamReader(System.in)); - NumberFormat parser = NumberFormat.getIntegerInstance(); - while (lost < ships.size()) { - System.out.print("\nTarget x,y\n> "); - String inputLine = input.readLine(); - if (inputLine == null) { - System.out.println("Game quit\n"); + if (! input.readCoordinates()) { return; } - String[] coords = inputLine.split(","); - if (coords.length != 2) { - System.out.println("Need two coordinates separated by ','"); - continue; - } - int[] xy = new int[2]; - boolean error = false; - try { - for (int c = 0 ; c < 2; ++c ) { - int val = Integer.parseInt(coords[c].strip()); - if ((val < 1) || (val > seaSize)) { - System.out.println("Coordinates must be from 1 to " + seaSize); - error = true; - } else { - xy[c] = val; - } - } - } - catch (NumberFormatException ne) { - System.out.println("Coordinates must be numbers"); - error = true; - } - if (error) continue; - int row = seaSize - xy[1]; - int col = xy[0] - 1; + int row = seaSize - input.y(); + int col = input.x() - 1; if (sea.isEmpty(col, row)) { ++misses; From 8e88e25d6c3fbc9fd783aa39d834fb10105fc0ec Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 19:46:37 +0000 Subject: [PATCH 07/13] Classes in own files --- 09_Battle/java/.gitignore | 1 + 09_Battle/java/Battle.java | 195 ------------------------------------- 09_Battle/java/Input.java | 58 +++++++++++ 09_Battle/java/Sea.java | 57 +++++++++++ 09_Battle/java/Ship.java | 142 +++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 195 deletions(-) create mode 100644 09_Battle/java/.gitignore create mode 100644 09_Battle/java/Input.java create mode 100644 09_Battle/java/Sea.java create mode 100644 09_Battle/java/Ship.java diff --git a/09_Battle/java/.gitignore b/09_Battle/java/.gitignore new file mode 100644 index 00000000..b25c15b8 --- /dev/null +++ b/09_Battle/java/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/09_Battle/java/Battle.java b/09_Battle/java/Battle.java index b548292c..627906d0 100644 --- a/09_Battle/java/Battle.java +++ b/09_Battle/java/Battle.java @@ -144,199 +144,4 @@ public class Battle { catch (IOException e) { } } - - private static 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; - - private int id; - private int size; - private String type; - private boolean placed; - private boolean sunk; - private ArrayList hits; - - private int startX; - private int startY; - private int orientX; - private int orientY; - - public Ship(int i, String name, int sz) { - id = i; type = name; size = sz; - sunk = false; placed = false; - hits = new ArrayList<>(Collections.nCopies(size, false)); - } - - public int id() { return id; } - public int size() { return size; } - - public void hit(int x, int y) { - int offset; - if (orientX != 0) { - offset = (x - startX) / orientX; - } else { - offset = (y - startY) / orientY; - } - hits.set(offset, true); - - sunk = hits.stream().allMatch(Predicate.isEqual(true)); - } - - public boolean isSunk() { return sunk; } - - public boolean wasHit(int x, int y) { - int offset; - if (orientX != 0) { - offset = (x - startX) / orientX; - } else { - offset = (y - startY) / orientY; - } - return hits.get(offset); - }; - - public void placeRandom(Sea s) { - Random random = new Random(); - for (int tries = 0 ; tries < 1000 ; ++tries) { - int x = random.nextInt(s.size()); - int y = random.nextInt(s.size()); - int orient = random.nextInt(4); - - if (place(s, x, y, orient)) return; - } - - throw new RuntimeException("Could not place any more ships"); - } - - 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 - - // we can extend the ship without colliding, but we are going diagonally - // and it should not be possible for two ships to cross each other on - // opposite diagonals. - - // check the two tiles that would cross us here - if either is empty, we are OK - // if they both contain different ships, we are OK - // but if they both contain the same ship, we are crossing! - int corner1 = s.get(fromX, toY); - int corner2 = s.get(toX, fromY); - 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; - } - - } - - private static class Sea { - private int tiles[]; - private boolean hits[]; - - private int size; - public Sea(int make_size) { - size = make_size; - tiles = new int[size*size]; - } - - public int size() { return size; } - - public String encodedDump() { - StringBuilder out = new StringBuilder(); - for (int x = 0; x < size; ++x) { - for (int y = 0; y < size; ++y) - out.append(Integer.toString(get(x, y))); - out.append('\n'); - } - return out.toString(); - } - - /* return true if x,y is in the sea and empty - * return false if x,y is occupied or is out of range - */ - public boolean isEmpty(int x, int y) { - if ((x<0)||(x>=size)||(y<0)||(y>=size)) return false; - return (get(x,y) == 0); - } - - /* return the ship number, or zero if no ship */ - public int get(int x, int y) { - return tiles[index(x,y)]; - } - - public void set(int x, int y, int value) { - tiles[index(x, y)] = value; - } - - public int shipHit(int x, int y) { - if (hits[index(x,y)]) return get(x, y); - else return 0; - } - - public void recordHit(int x, int y) { - hits[index(x, y)] = true; - } - - private int index(int x, int y) { - if ((x < 0) || (x >= size)) - throw new ArrayIndexOutOfBoundsException("Program error: x cannot be " + x); - if ((y < 0) || (y >= size)) - throw new ArrayIndexOutOfBoundsException("Program error: y cannot be " + y); - - return y*size + x; - } - } } diff --git a/09_Battle/java/Input.java b/09_Battle/java/Input.java new file mode 100644 index 00000000..8a782dba --- /dev/null +++ b/09_Battle/java/Input.java @@ -0,0 +1,58 @@ +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; +import java.text.NumberFormat; + +public class Input { + private BufferedReader reader; + private NumberFormat parser; + private int scale; + private boolean isQuit; + private int[] coords; + + public Input(int seaSize) { + scale = seaSize; + reader = new BufferedReader(new InputStreamReader(System.in)); + parser = NumberFormat.getIntegerInstance(); + } + + public boolean readCoordinates() throws IOException { + while (true) { + System.out.print("\nTarget x,y\n> "); + String inputLine = reader.readLine(); + if (inputLine == null) { + System.out.println("Game quit\n"); + isQuit = true; + return false; + } + + String[] fields = inputLine.split(","); + if (fields.length != 2) { + System.out.println("Need two coordinates separated by ','"); + continue; + } + + coords = new int[2]; + boolean error = false; + try { + for (int c = 0 ; c < 2; ++c ) { + int val = Integer.parseInt(fields[c].strip()); + if ((val < 1) || (val > scale)) { + System.out.println("Coordinates must be from 1 to " + scale); + error = true; + } else { + coords[c] = val; + } + } + } + catch (NumberFormatException ne) { + System.out.println("Coordinates must be numbers"); + error = true; + } + if (!error) return true; + } + } + + public int x() { return coords[0]; } + public int y() { return coords[1]; } +} diff --git a/09_Battle/java/Sea.java b/09_Battle/java/Sea.java new file mode 100644 index 00000000..d9987f34 --- /dev/null +++ b/09_Battle/java/Sea.java @@ -0,0 +1,57 @@ +class Sea { + private int tiles[]; + private boolean hits[]; + + private int size; + public Sea(int make_size) { + size = make_size; + tiles = new int[size*size]; + } + + public int size() { return size; } + + public String encodedDump() { + StringBuilder out = new StringBuilder(); + for (int x = 0; x < size; ++x) { + for (int y = 0; y < size; ++y) + out.append(Integer.toString(get(x, y))); + out.append('\n'); + } + return out.toString(); + } + + /* return true if x,y is in the sea and empty + * return false if x,y is occupied or is out of range + */ + public boolean isEmpty(int x, int y) { + if ((x<0)||(x>=size)||(y<0)||(y>=size)) return false; + return (get(x,y) == 0); + } + + /* return the ship number, or zero if no ship */ + public int get(int x, int y) { + return tiles[index(x,y)]; + } + + public void set(int x, int y, int value) { + tiles[index(x, y)] = value; + } + + public int shipHit(int x, int y) { + if (hits[index(x,y)]) return get(x, y); + else return 0; + } + + public void recordHit(int x, int y) { + hits[index(x, y)] = true; + } + + private int index(int x, int y) { + if ((x < 0) || (x >= size)) + throw new ArrayIndexOutOfBoundsException("Program error: x cannot be " + x); + if ((y < 0) || (y >= size)) + throw new ArrayIndexOutOfBoundsException("Program error: y cannot be " + y); + + return y*size + x; + } +} diff --git a/09_Battle/java/Ship.java b/09_Battle/java/Ship.java new file mode 100644 index 00000000..e3fb4f44 --- /dev/null +++ b/09_Battle/java/Ship.java @@ -0,0 +1,142 @@ +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Random; +import java.util.function.Predicate; + +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; + + private int id; + private int size; + private String type; + private boolean placed; + private boolean sunk; + private ArrayList hits; + + private int startX; + private int startY; + private int orientX; + private int orientY; + + public Ship(int i, String name, int sz) { + id = i; type = name; size = sz; + sunk = false; placed = false; + hits = new ArrayList<>(Collections.nCopies(size, false)); + } + + public int id() { return id; } + public int size() { return size; } + + public void hit(int x, int y) { + int offset; + if (orientX != 0) { + offset = (x - startX) / orientX; + } else { + offset = (y - startY) / orientY; + } + hits.set(offset, true); + + sunk = hits.stream().allMatch(Predicate.isEqual(true)); + } + + public boolean isSunk() { return sunk; } + + public boolean wasHit(int x, int y) { + int offset; + if (orientX != 0) { + offset = (x - startX) / orientX; + } else { + offset = (y - startY) / orientY; + } + return hits.get(offset); + }; + + public void placeRandom(Sea s) { + Random random = new Random(); + for (int tries = 0 ; tries < 1000 ; ++tries) { + int x = random.nextInt(s.size()); + int y = random.nextInt(s.size()); + int orient = random.nextInt(4); + + if (place(s, x, y, orient)) return; + } + + throw new RuntimeException("Could not place any more ships"); + } + + 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 + + // we can extend the ship without colliding, but we are going diagonally + // and it should not be possible for two ships to cross each other on + // opposite diagonals. + + // check the two tiles that would cross us here - if either is empty, we are OK + // if they both contain different ships, we are OK + // but if they both contain the same ship, we are crossing! + int corner1 = s.get(fromX, toY); + int corner2 = s.get(toX, fromY); + 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; + } +} + From 49be31b8e2f1086ed72565715ed3b3ee407b366f Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 20:15:15 +0000 Subject: [PATCH 08/13] Add some comments --- 09_Battle/java/Battle.java | 49 +++++++---- 09_Battle/java/Ship.java | 166 ++++++++++++++++++++++--------------- 2 files changed, 132 insertions(+), 83 deletions(-) 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; - } } - From 0614831f460cf286e698b177296e5862fdad39b9 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 20:22:14 +0000 Subject: [PATCH 09/13] Add comments to the sea class --- 09_Battle/java/Sea.java | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/09_Battle/java/Sea.java b/09_Battle/java/Sea.java index d9987f34..f0c31fac 100644 --- a/09_Battle/java/Sea.java +++ b/09_Battle/java/Sea.java @@ -1,8 +1,13 @@ +// Track the content of the sea class Sea { + // the sea is a square grid of tiles. It is a one-dimensional array, and this + // class maps x and y coordinates to an array index + // Each tile is either empty (value of tiles at index is 0) + // or contains a ship (value of tiles at index is the ship number) private int tiles[]; - private boolean hits[]; private int size; + public Sea(int make_size) { size = make_size; tiles = new int[size*size]; @@ -10,6 +15,8 @@ class Sea { public int size() { return size; } + // This writes out a representation of the sea, but in a funny order + // The idea is to give the player the job of working it out public String encodedDump() { StringBuilder out = new StringBuilder(); for (int x = 0; x < size; ++x) { @@ -22,13 +29,17 @@ class Sea { /* return true if x,y is in the sea and empty * return false if x,y is occupied or is out of range + * Doing this in one method makes placing ships much easier */ public boolean isEmpty(int x, int y) { if ((x<0)||(x>=size)||(y<0)||(y>=size)) return false; return (get(x,y) == 0); } - /* return the ship number, or zero if no ship */ + /* return the ship number, or zero if no ship. + * Unlike isEmpty(x,y), these other methods require that the + * coordinates passed be valid + */ public int get(int x, int y) { return tiles[index(x,y)]; } @@ -37,15 +48,7 @@ class Sea { tiles[index(x, y)] = value; } - public int shipHit(int x, int y) { - if (hits[index(x,y)]) return get(x, y); - else return 0; - } - - public void recordHit(int x, int y) { - hits[index(x, y)] = true; - } - + // map the coordinates to the array index private int index(int x, int y) { if ((x < 0) || (x >= size)) throw new ArrayIndexOutOfBoundsException("Program error: x cannot be " + x); From ea5c2cf72d3164654894431acb741ec971ea8a3b Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 10 Jan 2022 20:26:50 +0000 Subject: [PATCH 10/13] Comment the input class --- 09_Battle/java/Input.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/09_Battle/java/Input.java b/09_Battle/java/Input.java index 8a782dba..ee87465f 100644 --- a/09_Battle/java/Input.java +++ b/09_Battle/java/Input.java @@ -3,12 +3,15 @@ import java.io.InputStreamReader; import java.io.IOException; import java.text.NumberFormat; +// This class handles reading input from the player +// Each input is an x and y coordinate +// e.g. 5,3 public class Input { private BufferedReader reader; private NumberFormat parser; - private int scale; - private boolean isQuit; - private int[] coords; + private int scale; // size of the sea, needed to validate input + private boolean isQuit; // whether the input has ended + private int[] coords; // the last coordinates read public Input(int seaSize) { scale = seaSize; @@ -18,22 +21,27 @@ public class Input { public boolean readCoordinates() throws IOException { while (true) { + // Write a prompt System.out.print("\nTarget x,y\n> "); String inputLine = reader.readLine(); if (inputLine == null) { - System.out.println("Game quit\n"); + // If the input stream is ended, there is no way to continue the game + System.out.println("\nGame quit\n"); isQuit = true; return false; } + // split the input into two fields String[] fields = inputLine.split(","); if (fields.length != 2) { + // has to be exactly two System.out.println("Need two coordinates separated by ','"); continue; } coords = new int[2]; boolean error = false; + // each field should contain an integer from 1 to the size of the sea try { for (int c = 0 ; c < 2; ++c ) { int val = Integer.parseInt(fields[c].strip()); @@ -46,6 +54,7 @@ public class Input { } } catch (NumberFormatException ne) { + // this happens if the field is not a valid number System.out.println("Coordinates must be numbers"); error = true; } From 00a8bf8983b0cc36a8b364908ce559d3f387e249 Mon Sep 17 00:00:00 2001 From: Josh Gribbon Date: Mon, 10 Jan 2022 15:42:08 -0500 Subject: [PATCH 11/13] Move file --- .../find-missing-implementations.js | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) rename find-missing-implementations.js => 00_Utilities/find-missing-implementations.js (79%) diff --git a/find-missing-implementations.js b/00_Utilities/find-missing-implementations.js similarity index 79% rename from find-missing-implementations.js rename to 00_Utilities/find-missing-implementations.js index 04cf3f84..1f998444 100644 --- a/find-missing-implementations.js +++ b/00_Utilities/find-missing-implementations.js @@ -15,7 +15,7 @@ const ROOT_PATH = "."; const languages = [ { name: "csharp", extension: "cs" }, { name: "java", extension: "java" }, - { name: "javascript", extension: "js" }, + { name: "javascript", extension: "html" }, { name: "pascal", extension: "pas" }, { name: "perl", extension: "pl" }, { name: "python", extension: "py" }, @@ -45,6 +45,7 @@ const getPuzzleFolders = () => { (async () => { let missingGames = {}; let missingLanguageCounts = {}; + languages.forEach((l) => (missingLanguageCounts[l.name] = 0)); const puzzles = getPuzzleFolders(); for (const puzzle of puzzles) { for (const { name: language, extension } of languages) { @@ -53,12 +54,8 @@ const getPuzzleFolders = () => { extension ); if (files.length === 0) { - if (!missingGames[puzzle]) { - missingGames[puzzle] = []; - } - if (!missingLanguageCounts[language]) { - missingLanguageCounts[language] = 0; - } + if (!missingGames[puzzle]) missingGames[puzzle] = []; + missingGames[puzzle].push(language); missingLanguageCounts[language]++; } @@ -70,15 +67,14 @@ const getPuzzleFolders = () => { } else { console.log(`Missing ${missingCount} implementations:`); - console.log(`\nMissing languages by game:`); - for (const [puzzle, languages] of Object.entries(missingGames)) { - console.log(`${puzzle}: ${languages.join(", ")}`); - } + Object.entries(missingGames).forEach( + ([p, ls]) => (missingGames[p] = ls.join(", ")) + ); + console.log(`\nMissing languages by game:`); + console.table(missingGames); console.log(`\nBy language:`); - for (const [language, count] of Object.entries(missingLanguageCounts)) { - console.log(`${language}: ${count} missing`); - } + console.table(missingLanguageCounts); } })(); From 2d2df367491a06003a9f5147cc1a80fc2e9ea1e5 Mon Sep 17 00:00:00 2001 From: Josh Gribbon Date: Mon, 10 Jan 2022 15:44:23 -0500 Subject: [PATCH 12/13] Fix relative path --- 00_Utilities/find-missing-implementations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/00_Utilities/find-missing-implementations.js b/00_Utilities/find-missing-implementations.js index 1f998444..bd92bd79 100644 --- a/00_Utilities/find-missing-implementations.js +++ b/00_Utilities/find-missing-implementations.js @@ -10,7 +10,7 @@ const fs = require("fs"); const glob = require("glob"); // relative path to the repository root -const ROOT_PATH = "."; +const ROOT_PATH = "../."; const languages = [ { name: "csharp", extension: "cs" }, From 01fb3bd5009102e5bff45395981d047eccbb8756 Mon Sep 17 00:00:00 2001 From: Josh Gribbon Date: Mon, 10 Jan 2022 15:46:51 -0500 Subject: [PATCH 13/13] Skip utilities folder --- 00_Utilities/find-missing-implementations.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/00_Utilities/find-missing-implementations.js b/00_Utilities/find-missing-implementations.js index bd92bd79..61d849fe 100644 --- a/00_Utilities/find-missing-implementations.js +++ b/00_Utilities/find-missing-implementations.js @@ -38,7 +38,10 @@ const getPuzzleFolders = () => { return fs .readdirSync(ROOT_PATH, { withFileTypes: true }) .filter((dirEntry) => dirEntry.isDirectory()) - .filter((dirEntry) => ![".git", "node_modules"].includes(dirEntry.name)) + .filter( + (dirEntry) => + ![".git", "node_modules", "00_Utilities"].includes(dirEntry.name) + ) .map((dirEntry) => dirEntry.name); };