Merge branch 'coding-horror:main' into 75_Roulette_Java

This commit is contained in:
Thomas Kwashnak
2022-01-10 16:10:21 -05:00
committed by GitHub
8 changed files with 609 additions and 18 deletions

View File

@@ -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;

1
09_Battle/java/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*~

168
09_Battle/java/Battle.java Normal file
View File

@@ -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<Ship> 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<Ship>(); // 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<Ship> 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<String> 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);
}
}
}

67
09_Battle/java/Input.java Normal file
View File

@@ -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]; }
}

60
09_Battle/java/Sea.java Normal file
View File

@@ -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;
}
}

170
09_Battle/java/Ship.java Normal file
View File

@@ -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<Boolean> 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;
}
}

View File

@@ -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 <filename>.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

View File

@@ -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: