diff --git a/00_Utilities/jvmTestUtils/kotlin/test/ConsoleTest.kt b/00_Utilities/jvmTestUtils/kotlin/test/ConsoleTest.kt new file mode 100644 index 00000000..d58af170 --- /dev/null +++ b/00_Utilities/jvmTestUtils/kotlin/test/ConsoleTest.kt @@ -0,0 +1,37 @@ +package com.pcholt.console.testutils + +import com.google.common.truth.Truth +import org.junit.Rule +import org.junit.contrib.java.lang.system.SystemOutRule +import org.junit.contrib.java.lang.system.TextFromStandardInputStream + +abstract class ConsoleTest { + @get:Rule + val inputRule = TextFromStandardInputStream.emptyStandardInputStream() + + @get:Rule + val systemOutRule = SystemOutRule().enableLog() + + val regexInputCommand = "\\{(.*)}".toRegex() + + fun assertConversation(conversation: String, runMain: () -> Unit) { + + inputRule.provideLines(*regexInputCommand + .findAll(conversation) + .map { it.groupValues[1] } + .toList().toTypedArray()) + + runMain() + + Truth.assertThat( + systemOutRule.log.trimWhiteSpace() + ) + .isEqualTo( + regexInputCommand + .replace(conversation, "").trimWhiteSpace() + ) + } + + private fun String.trimWhiteSpace() = + replace("[\\s]+".toRegex(), " ") +} \ No newline at end of file diff --git a/03_Animal/java/Animal.java b/03_Animal/java/src/Animal.java similarity index 98% rename from 03_Animal/java/Animal.java rename to 03_Animal/java/src/Animal.java index 681425c1..c4222c5f 100644 --- a/03_Animal/java/Animal.java +++ b/03_Animal/java/src/Animal.java @@ -74,11 +74,11 @@ public class Animal { private static void askForInformationAndSave(Scanner scan, AnimalNode current, QuestionNode previous, boolean previousToCurrentDecisionChoice) { //Failed to get it right and ran out of questions //Let's ask the user for the new information - System.out.print("THE ANIMAL YOU WERE THINKING OF WAS A "); + System.out.print("THE ANIMAL YOU WERE THINKING OF WAS A ? "); String animal = scan.nextLine(); - System.out.printf("PLEASE TYPE IN A QUESTION THAT WOULD DISTINGUISH A %s FROM A %s ", animal, current.getAnimal()); + System.out.printf("PLEASE TYPE IN A QUESTION THAT WOULD DISTINGUISH A %s FROM A %s ? ", animal, current.getAnimal()); String newQuestion = scan.nextLine(); - System.out.printf("FOR A %s THE ANSWER WOULD BE ", animal); + System.out.printf("FOR A %s THE ANSWER WOULD BE ? ", animal); boolean newAnswer = readYesOrNo(scan); //Add it to our question store addNewAnimal(current, previous, animal, newQuestion, newAnswer, previousToCurrentDecisionChoice); diff --git a/03_Animal/java/test/AnimalJavaTest.kt b/03_Animal/java/test/AnimalJavaTest.kt new file mode 100644 index 00000000..013655a3 --- /dev/null +++ b/03_Animal/java/test/AnimalJavaTest.kt @@ -0,0 +1,55 @@ +import com.pcholt.console.testutils.ConsoleTest +import org.junit.Test + +class AnimalJavaTest : ConsoleTest() { + + @Test + fun `given a standard setup, find the fish`() { + assertConversation( + """ + $title + ARE YOU THINKING OF AN ANIMAL ? {YES} + DOES IT SWIM ? {YES} + IS IT A FISH ? {YES} + WHY NOT TRY ANOTHER ANIMAL? + ARE YOU THINKING OF AN ANIMAL ? {QUIT} + """ + ) { + Animal.main(emptyArray()) + } + } + + @Test + fun `given a standard setup, create a cow, and verify`() { + assertConversation( + """ + $title + ARE YOU THINKING OF AN ANIMAL ? {YES} + DOES IT SWIM ? {NO} + IS IT A BIRD ? {NO} + THE ANIMAL YOU WERE THINKING OF WAS A ? {COW} + PLEASE TYPE IN A QUESTION THAT WOULD DISTINGUISH A + COW FROM A BIRD + ? {DOES IT EAT GRASS} + FOR A COW THE ANSWER WOULD BE ? {YES} + ARE YOU THINKING OF AN ANIMAL ? {YES} + DOES IT SWIM ? {NO} + DOES IT EAT GRASS ? {YES} + IS IT A COW ? {YES} + WHY NOT TRY ANOTHER ANIMAL? + ARE YOU THINKING OF AN ANIMAL ? {QUIT} + """ + ) { + Animal.main(emptyArray()) + } + } + + private val title = """ + ANIMAL + CREATIVE COMPUTING MORRISTOWN, NEW JERSEY + + PLAY 'GUESS THE ANIMAL' + THINK OF AN ANIMAL AND THE COMPUTER WILL TRY TO GUESS IT. + """ +} + diff --git a/03_Animal/kotlin/Animal.kt b/03_Animal/kotlin/src/Animal.kt similarity index 100% rename from 03_Animal/kotlin/Animal.kt rename to 03_Animal/kotlin/src/Animal.kt diff --git a/03_Animal/kotlin/test/AnimalKtTest.kt b/03_Animal/kotlin/test/AnimalKtTest.kt new file mode 100644 index 00000000..e93857ea --- /dev/null +++ b/03_Animal/kotlin/test/AnimalKtTest.kt @@ -0,0 +1,55 @@ +import com.pcholt.console.testutils.ConsoleTest +import org.junit.Test + +class AnimalKtTest : ConsoleTest() { + + @Test + fun `given a standard setup, find the fish`() { + assertConversation( + """ + $title + ARE YOU THINKING OF AN ANIMAL? {YES} + DOES IT SWIM? {YES} + IS IT A FISH? {YES} + WHY NOT TRY ANOTHER ANIMAL? + ARE YOU THINKING OF AN ANIMAL? {QUIT} + """ + ) { + main() + } + } + + @Test + fun `given a standard setup, create a cow, and verify`() { + assertConversation( + """ + $title + ARE YOU THINKING OF AN ANIMAL? {YES} + DOES IT SWIM? {NO} + IS IT A BIRD? {NO} + THE ANIMAL YOU WERE THINKING OF WAS A? {COW} + PLEASE TYPE IN A QUESTION THAT WOULD DISTINGUISH A + COW FROM A BIRD + ? {DOES IT EAT GRASS} + FOR A COW THE ANSWER WOULD BE? {YES} + ARE YOU THINKING OF AN ANIMAL? {YES} + DOES IT SWIM? {NO} + DOES IT EAT GRASS? {YES} + IS IT A COW? {YES} + WHY NOT TRY ANOTHER ANIMAL? + ARE YOU THINKING OF AN ANIMAL? {QUIT} + """ + ) { + main() + } + } + + private val title = """ + ANIMAL + CREATIVE COMPUTING MORRISTOWN, NEW JERSEY + + PLAY 'GUESS THE ANIMAL' + THINK OF AN ANIMAL AND THE COMPUTER WILL TRY TO GUESS IT. + """ +} + diff --git a/55_Life/java/README.md b/55_Life/java/README.md index 51edd8d4..19a5ff38 100644 --- a/55_Life/java/README.md +++ b/55_Life/java/README.md @@ -1,3 +1,19 @@ +# Game of Life - Java version + Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Oracle Java](https://openjdk.java.net/) + +## Requirements + +* Requires Java 17 (or later) + +## Notes + +The Java version of Game of Life tries to mimics the behaviour of the BASIC version. +However, the Java code does not have much in common with the original. + +**Differences in behaviour:** +* Input supports the ```.``` character, but it's optional. +* Evaluation of ```DONE``` input string is case insensitive. +* Run with the ```-s``` command line argument to halt the program after each generation, and continue when ```ENTER``` is pressed. \ No newline at end of file diff --git a/55_Life/java/src/java/Life.java b/55_Life/java/src/java/Life.java new file mode 100644 index 00000000..2f5c184a --- /dev/null +++ b/55_Life/java/src/java/Life.java @@ -0,0 +1,191 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * The Game of Life class.
+ *
+ * Mimics the behaviour of the BASIC version, however the Java code does not have much in common with the original. + *
+ * Differences in behaviour: + * + */ +public class Life { + + private static final byte DEAD = 0; + private static final byte ALIVE = 1; + private static final String NEWLINE = "\n"; + + private final Scanner consoleReader = new Scanner(System.in); + + private final byte[][] matrix = new byte[21][67]; + private int generation = 0; + private int population = 0; + boolean stopAfterGen = false; + boolean invalid = false; + + /** + * Constructor. + * + * @param args the command line arguments + */ + public Life(String[] args) { + parse(args); + } + + private void parse(String[] args) { + for (String arg : args) { + if ("-s".equals(arg)) { + stopAfterGen = true; + break; + } + } + } + + /** + * Starts the game. + */ + public void start() { + printGameHeader(); + readPattern(); + while (true) { + printGeneration(); + advanceToNextGeneration(); + if (stopAfterGen) { + System.out.print("PRESS ENTER TO CONTINUE"); + consoleReader.nextLine(); + } + } + } + + private void advanceToNextGeneration() { + // store all cell transitions in a list, i.e. if a dead cell becomes alive, or a living cell dies + List transitions = new ArrayList<>(); + // there's still room for optimization: instead of iterating over all cells in the matrix, + // we could consider only the section containing the pattern(s), as in the BASIC version + for (int y = 0; y < matrix.length; y++) { + for (int x = 0; x < matrix[y].length; x++) { + int neighbours = countNeighbours(y, x); + if (matrix[y][x] == ALIVE) { + if (neighbours < 2 || neighbours > 3) { + transitions.add(new Transition(y, x, DEAD)); + population--; + } + } else { // cell is dead + if (neighbours == 3) { + if (x < 2 || x > 67 || y < 2 || y > 21) { + invalid = true; + } + transitions.add(new Transition(y, x, ALIVE)); + population++; + } + } + } + } + // apply all transitions to the matrix + transitions.forEach(t -> matrix[t.y()][t.x()] = t.newState()); + generation++; + } + + private int countNeighbours(int y, int x) { + int neighbours = 0; + for (int row = Math.max(y - 1, 0); row <= Math.min(y + 1, matrix.length - 1); row++) { + for (int col = Math.max(x - 1, 0); col <= Math.min(x + 1, matrix[row].length - 1); col++) { + if (row == y && col == x) { + continue; + } + if (matrix[row][col] == ALIVE) { + neighbours++; + } + } + } + return neighbours; + } + + private void readPattern() { + System.out.println("ENTER YOUR PATTERN:"); + List lines = new ArrayList<>(); + String line; + int maxLineLength = 0; + boolean reading = true; + while (reading) { + System.out.print("? "); + line = consoleReader.nextLine(); + if (line.equalsIgnoreCase("done")) { + reading = false; + } else { + // optional support for the '.' that is needed in the BASIC version + lines.add(line.replace('.', ' ')); + maxLineLength = Math.max(maxLineLength, line.length()); + } + } + fillMatrix(lines, maxLineLength); + } + + private void fillMatrix(List lines, int maxLineLength) { + float xMin = 33 - maxLineLength / 2f; + float yMin = 11 - lines.size() / 2f; + for (int y = 0; y < lines.size(); y++) { + String line = lines.get(y); + for (int x = 1; x <= line.length(); x++) { + if (line.charAt(x-1) == '*') { + matrix[floor(yMin + y)][floor(xMin + x)] = ALIVE; + population++; + } + } + } + } + + private int floor(float f) { + return (int) Math.floor(f); + } + + private void printGameHeader() { + printIndented(34, "LIFE"); + printIndented(15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + System.out.println(NEWLINE.repeat(3)); + } + + private void printIndented(int spaces, String str) { + System.out.println(" ".repeat(spaces) + str); + } + + private void printGeneration() { + printGenerationHeader(); + for (int y = 0; y < matrix.length; y++) { + for (int x = 0; x < matrix[y].length; x++) { + System.out.print(matrix[y][x] == 1 ? "*" : " "); + } + System.out.println(); + } + } + + private void printGenerationHeader() { + String invalidText = invalid ? "INVALID!" : ""; + System.out.printf("GENERATION: %-13d POPULATION: %d %s\n", generation, population, invalidText); + } + + /** + * Main method that starts the program. + * + * @param args the command line arguments: + *
-s: Stop after each generation (press enter to continue)
+ * @throws Exception if something goes wrong. + */ + public static void main(String[] args) throws Exception { + new Life(args).start(); + } + +} + +/** + * Represents a state change for a single cell within the matrix. + * + * @param y the y coordinate (row) of the cell + * @param x the x coordinate (column) of the cell + * @param newState the new state of the cell (either DEAD or ALIVE) + */ +record Transition(int y, int x, byte newState) { } diff --git a/85_Synonym/csharp/Synonym.cs b/85_Synonym/csharp/Synonym.cs new file mode 100644 index 00000000..9d58da71 --- /dev/null +++ b/85_Synonym/csharp/Synonym.cs @@ -0,0 +1,149 @@ +using System.Text; + +namespace Synonym +{ + class Synonym + { + Random rand = new Random(); + + // Initialize list of corrent responses + private string[] Affirmations = { "Right", "Correct", "Fine", "Good!", "Check" }; + + // Initialize list of words and their synonyms + private string[][] Words = + { + new string[] {"first", "start", "beginning", "onset", "initial"}, + new string[] {"similar", "alike", "same", "like", "resembling"}, + new string[] {"model", "pattern", "prototype", "standard", "criterion"}, + new string[] {"small", "insignificant", "little", "tiny", "minute"}, + new string[] {"stop", "halt", "stay", "arrest", "check", "standstill"}, + new string[] {"house", "dwelling", "residence", "domicile", "lodging", "habitation"}, + new string[] {"pit", "hole", "hollow", "well", "gulf", "chasm", "abyss"}, + new string[] {"push", "shove", "thrust", "prod", "poke", "butt", "press"}, + new string[] {"red", "rouge", "scarlet", "crimson", "flame", "ruby"}, + new string[] {"pain", "suffering", "hurt", "misery", "distress", "ache", "discomfort"} + }; + + private void DisplayIntro() + { + Console.WriteLine(""); + Console.WriteLine("SYNONYM".PadLeft(23)); + Console.WriteLine("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + Console.WriteLine(""); + Console.WriteLine("A synonym of a word means another word in the English"); + Console.WriteLine("language which has the same or very nearly the same meaning."); + Console.WriteLine("I choose a word -- you type a synonym."); + Console.WriteLine("If you can't think of a synonym, type the word 'help'"); + Console.WriteLine("and I will tell you a synonym."); + Console.WriteLine(""); + } + + private void DisplayOutro() + { + Console.WriteLine("Synonym drill completed."); + } + + private void RandomizeTheList() + { + // Randomize the list of Words to pick from + int[] Order = new int[Words.Length]; + foreach (int i in Order) + { + Order[i] = rand.Next(); + } + Array.Sort(Order, Words); + } + + private string GetAnAffirmation() + { + return Affirmations[rand.Next(Affirmations.Length)]; + } + + private bool CheckTheResponse(string WordName, int WordIndex, string LineInput, string[] WordList) + { + if (LineInput.Equals("help")) + { + // Choose a random correct synonym response that doesn't equal the current word given + int HelpIndex = rand.Next(WordList.Length); + while (HelpIndex == WordIndex) + { + HelpIndex = rand.Next(0, WordList.Length); + } + Console.WriteLine("**** A synonym of {0} is {1}.", WordName, WordList[HelpIndex]); + + return false; + } + else + { + // Check to see if the response is one of the listed synonyms and not the current word prompt + if (WordList.Contains(LineInput) && LineInput != WordName) + { + // Randomly display one of the five correct answer exclamations + Console.WriteLine(GetAnAffirmation()); + + return true; + } + else + { + // Incorrect response. Try again. + Console.WriteLine(" Try again.".PadLeft(5)); + + return false; + } + } + } + + private string PromptForSynonym(string WordName) + { + Console.Write(" What is a synonym of {0}? ", WordName); + string LineInput = Console.ReadLine().Trim().ToLower(); + + return LineInput; + } + + private void AskForSynonyms() + { + Random rand = new Random(); + + // Loop through the now randomized list of Words and display a random word from each to prompt for a synonym + foreach (string[] WordList in Words) + { + int WordIndex = rand.Next(WordList.Length); // random word position in the current list of words + string WordName = WordList[WordIndex]; // what is that actual word + bool Success = false; + + while (!Success) + { + // Ask for the synonym of the current word + string LineInput = PromptForSynonym(WordName); + + // Check the response + Success = CheckTheResponse(WordName, WordIndex, LineInput, WordList); + + // Add extra line space for formatting + Console.WriteLine(""); + } + } + } + + public void PlayTheGame() + { + RandomizeTheList(); + + DisplayIntro(); + + AskForSynonyms(); + + DisplayOutro(); + } + } + class Program + { + static void Main(string[] args) + { + + new Synonym().PlayTheGame(); + + } + } +} \ No newline at end of file diff --git a/85_Synonym/csharp/Synonym.csproj b/85_Synonym/csharp/Synonym.csproj index d3fe4757..1fd332a6 100644 --- a/85_Synonym/csharp/Synonym.csproj +++ b/85_Synonym/csharp/Synonym.csproj @@ -1,9 +1,10 @@ - + Exe net6.0 10 enable enable + Synonym.Program diff --git a/buildJvm/README.md b/buildJvm/README.md index 4ecba004..c08811b9 100644 --- a/buildJvm/README.md +++ b/buildJvm/README.md @@ -67,16 +67,16 @@ directory for the java or kotlin file, and the class that contains the `main` me The `build.gradle` file **should** be identical to all the other `build.gradle` files in all the other subprojects: ```groovy - sourceSets { - main { - java { - srcDirs "../../$gameSource" - } - } - } - application { - mainClass = gameMain - } + sourceSets { + main { + java { + srcDirs "../../$gameSource" + } + } + } + application { + mainClass = gameMain + } ``` The `gradle.properties` file should look like this: @@ -92,3 +92,84 @@ project to the list. ```groovy include ":build_91_Train_java" ``` + +### Adding a game with tests + +You can add tests for JVM games with a `build.gradle` looking a little different. +Use the build files from `03_Animal` as a template to add tests: + +```groovy +sourceSets { + main { + java { + srcDirs "../../$gameSource" + } + } + test { + java { + srcDirs "../../$gameTest" + } + } +} + +application { + mainClass = gameMain +} + +dependencies { + testImplementation(project(":build_00_utilities").sourceSets.test.output) +} +``` + +The gradle.properties needs an additional directory name for the tests, as `gameTest` : +``` +gameSource=03_Animal/java/src +gameTest=03_Animal/java/test +gameMain=Animal +``` + +Each project should have its own test, and shouldn't share test source directories +with other projects, even if they are for the same game. + +Tests are constructed by subclassing `ConsoleTest`. This allows you to use the +`assertConversation` function to check for correct interactive conversations. +```kotlin +import com.pcholt.console.testutils.ConsoleTest +import org.junit.Test + +class AnimalJavaTest : ConsoleTest() { + @Test + fun `should have a simple conversation`() { + assertConversation( + """ + WHAT'S YOUR NAME? {PAUL} + YOUR NAME IS PAUL? {YES} + THANKS FOR PLAYING + """ + ) { + // The game's Main method + main() + } + } +} +``` + +Curly brackets are the expected user input. +Note - this is actually just a way of defining the expected input as "PAUL" and "YES" +and not that the input happens at the exact prompt position. Thus this is equivalent: +```kotlin +""" +{PAUL} {YES} WHAT'S YOUR NAME? +YOUR NAME IS PAUL? +THANKS FOR PLAYING +""" +``` + +Amounts of whitespace are not counted, but whitespace is significant: You will get a failure if +your game emits `"NAME?"` when it expects `"NAME ?"`. + +Run all the tests from within the buildJvm project directory: +```bash +cd buildJvm +./gradlew test +```