diff --git a/00_Utilities/find-missing-implementations.js b/00_Utilities/find-missing-implementations.js new file mode 100644 index 00000000..61d849fe --- /dev/null +++ b/00_Utilities/find-missing-implementations.js @@ -0,0 +1,84 @@ +/** + * 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: "html" }, + { 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", "00_Utilities"].includes(dirEntry.name) + ) + .map((dirEntry) => dirEntry.name); +}; + +(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) { + const files = await getFilesRecursive( + `${ROOT_PATH}/${puzzle}/${language}`, + extension + ); + if (files.length === 0) { + if (!missingGames[puzzle]) missingGames[puzzle] = []; + + 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:`); + + Object.entries(missingGames).forEach( + ([p, ls]) => (missingGames[p] = ls.join(", ")) + ); + + console.log(`\nMissing languages by game:`); + console.table(missingGames); + console.log(`\nBy language:`); + console.table(missingLanguageCounts); + } +})(); + +return; 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 new file mode 100644 index 00000000..c0b27906 --- /dev/null +++ b/09_Battle/java/Battle.java @@ -0,0 +1,168 @@ +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; + + +/* 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; + + /* 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", + "destroyer", + "cruiser", + "aircraft carrier", + "size5" }; + + // Entrypoint + public static void main(String args[]) { + 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(); + } + + 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"); + } + + // 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++, 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); + } + } + + 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"); + Input input = new Input(seaSize); + try { + 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; + + 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 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."); + 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) { + // 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/Input.java b/09_Battle/java/Input.java new file mode 100644 index 00000000..ee87465f --- /dev/null +++ b/09_Battle/java/Input.java @@ -0,0 +1,67 @@ +import java.io.BufferedReader; +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; // 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; + reader = new BufferedReader(new InputStreamReader(System.in)); + parser = NumberFormat.getIntegerInstance(); + } + + public boolean readCoordinates() throws IOException { + while (true) { + // Write a prompt + System.out.print("\nTarget x,y\n> "); + String inputLine = reader.readLine(); + if (inputLine == null) { + // 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()); + if ((val < 1) || (val > scale)) { + System.out.println("Coordinates must be from 1 to " + scale); + error = true; + } else { + coords[c] = val; + } + } + } + catch (NumberFormatException ne) { + // this happens if the field is not a valid number + 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..f0c31fac --- /dev/null +++ b/09_Battle/java/Sea.java @@ -0,0 +1,60 @@ +// 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 int size; + + public Sea(int make_size) { + size = make_size; + tiles = new int[size*size]; + } + + 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) { + 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 + * 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. + * 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)]; + } + + public void set(int x, int y, int value) { + tiles[index(x, y)] = value; + } + + // 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); + 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..23605e5c --- /dev/null +++ b/09_Battle/java/Ship.java @@ -0,0 +1,170 @@ +import java.util.ArrayList; +import java.util.Collections; +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 { + // 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; // 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; // starting position coordinates + private int startY; + private int orientX; // x and y deltas from each tile occupied to the next + private int orientY; + + 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; + } else { + offset = (y - startY) / orientY; + } + 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) { + offset = (x - startX) / orientX; + } else { + offset = (y - startY) / orientY; + } + 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) { + 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"); + } + + // 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 + + // 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; + } +} diff --git a/HOW_TO_RUN_THE_GAMES.md b/HOW_TO_RUN_THE_GAMES.md index 6df23205..fae0656f 100644 --- a/HOW_TO_RUN_THE_GAMES.md +++ b/HOW_TO_RUN_THE_GAMES.md @@ -23,16 +23,19 @@ 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) + 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 +44,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 +56,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 +65,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 +73,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 +88,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..a5169abd 100644 --- a/buildJvm/README.md +++ b/buildJvm/README.md @@ -1,25 +1,56 @@ # 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 +``` + +--- +## 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. -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. --- ## Adding a new game @@ -34,9 +65,10 @@ 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 } sourceSets { @@ -54,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: