Merge branch 'coding-horror:main' into main

This commit is contained in:
Steve Bosman
2022-01-29 17:25:50 +00:00
committed by GitHub
10 changed files with 599 additions and 14 deletions

View File

@@ -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(), " ")
}

View File

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

View File

@@ -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.
"""
}

View File

@@ -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.
"""
}

View File

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

View File

@@ -0,0 +1,191 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* The Game of Life class.<br>
* <br>
* Mimics the behaviour of the BASIC version, however the Java code does not have much in common with the original.
* <br>
* Differences in behaviour:
* <ul>
* <li>Input supports the "." character, but it's optional.</li>
* <li>Input regarding the "DONE" string is case insensitive.</li>
* </ul>
*/
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<Transition> 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<String> 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<String> 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:
* <pre>-s: Stop after each generation (press enter to continue)</pre>
* @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) { }

View File

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

View File

@@ -1,9 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<StartupObject>Synonym.Program</StartupObject>
</PropertyGroup>
</Project>

View File

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