# 21: The number with the curse

The goal of this game is to not say the number 21.

Rules:

1. You must only say numbers consecutive to the previous said number.
2. You may say 1, 2, or 3 numbers at a time.

For example:

Player 1 says 1
Player 2 says 2, 3
Player 1 says 4, 5, 6
Player 2 says 7, 8
And so on...

You will be playing AI, and you will start. Try your best, but no matter how hard you try, you will lose!

I will personally give the one who can find a way to beat the AI (i.e. a bug in the program) with a 50 rep bounty.

Main.java

public class Main {

private static final int END = 21;

public static void main(String[] args) {
Game game = new Game(END);
game.startGame();
}

}


Game.java

import java.util.Scanner;
import java.util.regex.Pattern;

public class Game {

private static final Pattern NUMBER_LIST_PATTERN = Pattern.compile("(\\d+,)*\\d+");
private static final String HELP = "help";
private static final int MAX_NUMS_PER_TURN = 3;

private final int end;

public Game(int end) {
if (end % 4 != 1) {
throw new IllegalArgumentException("Nice try, this number will fail the AI.");
}
this.end = end;
}

public void startGame() {
Scanner scanner = new Scanner(System.in);
AI impossibleComputer = new AI();
displayHelp();
do {
boolean hasLost = false;
int previousNumber = 0;
do {
int[] numbers = getNumbers(scanner, previousNumber);
if (contains(numbers, end)) {
System.out.println("You lose!");
hasLost = true;
} else {
previousNumber = printAINumbers(impossibleComputer.getNextNumbers(numbers[numbers.length - 1]));
}
} while (!hasLost);

} while (doAgain(scanner));
System.out.println("Thanks for playing!");
}

private void displayHelp() {
System.out.println("The goal of this game is to not say the number 21.\n\n" + "Rules:\n"
+ "1. You must only say numbers consecutive to the previous said number.\n"
+ "2. You may say 1, 2, or 3 numbers at a time.\n\n" + "For example:\n\n" + "\tPlayer 1 says 1\n"
+ "\tPlayer 2 says 2, 3\n" + "\tPlayer 1 says 4, 5, 6\n" + "\tPlayer 2 says 7, 8\n"
+ "\tAnd so on...\n\n"
+ "You will be playing AI, and you will start. Try your best, but no matter how hard you try, you will lose!\n");
}

private int[] getNumbers(Scanner scanner, int previousNumber) {
System.out
.println("Enter your numbers, separated by commas only (e.g. \"1,2,3\"), or enter \"help\" for help.\n"
+ "Note that if you enter more than three numbers, this program will ignore the ones after the third one.");
while (true) {
String input = scanner.nextLine().trim();
if (input.equals(HELP)) {
displayHelp();
return null;
} else {
if (NUMBER_LIST_PATTERN.matcher(input).matches()) {
int[] result = toIntArray(input);
if (result != null && result[0] == previousNumber + 1) {
return result;
}
}
System.out.println("Oops, illegal input. Try again:");
}
}
}

private int[] toIntArray(String input) {
String[] array = input.split(",");
int length = array.length;
int[] result = new int[length > MAX_NUMS_PER_TURN ? MAX_NUMS_PER_TURN : length];
for (int i = 0; i < length && i < MAX_NUMS_PER_TURN; i++) {
result[i] = Integer.parseInt(array[i]);
if (i != 0 && result[i] != result[i - 1] + 1) {
return null;
}
}
return result;
}

private boolean contains(int[] numbers, int number) {
for (int num : numbers) {
if (num == number) {
return true;
}
}
return false;
}

private int printAINumbers(int[] nextNumbers) {
System.out.print("The AI says: ");
for (int number : nextNumbers) {
System.out.print(number + "  ");
}
System.out.println();
return nextNumbers[nextNumbers.length - 1];
}

private boolean doAgain(Scanner scanner) {
System.out.println("Do you want to play again?");
while (true) {
char in = Character.toUpperCase(scanner.next().charAt(0));
if (in == 'Y') {
return true;
} else if (in == 'N') {
return false;
}
System.out.println("Oops, that was not valid input. Try again: ");
}
}

}


Now comes the part to reveal the secret...

AI.java

public class AI {

public int[] getNextNumbers(int lastNumber) {
if (lastNumber % 4 == 0) {
return new int[] {lastNumber + 1};
}
return list(lastNumber + 1, 4 * ((int) (lastNumber / 4) + 1));
}

private int[] list(int x, int y) {
int[] result = new int[y - x + 1];
for (int i = 0; x + i <= y; i++) {
result[i] = x + i;
}
return result;
}

}


If you don't understand why this works, all the AI does is counts up to the next multiple of 4. Why does this work? Well, this is what happens:

Player: 1, [2], [3]
AI:     [2], [3], 4
Player: 5, [6], [7]
AI:     [6], [7], 8
Player: 9, [10], [11]
AI:     [10], [11], 12
Player: 13, [14], [15]
AI:     [14], [15], 16
Player: 17, [18], [19]
AI:     [18], [19], 20
Player: 21


Anything in the square brackets, if not said by the player, will be said by AI. No matter how you play, the AI will win (as long as it starts).

Concerns:

1. Am I doing well OOP-wise?
2. Are my variable names good?
3. And as usual, anything else?

It's a two-player game. Both the AI and the human player should implement a common Player interface. That would make it easy to convert it into a human-human game, for example.

Input validation happens partly in getNumbers() (the regex match and a consecutive numbers check) and partly in toIntArray() (another consecutive numbers check). It would be nice if the validation all happened in the same place.

An equivalent game would just ask each player to pick 1, 2, or 3 to be added to the current sum. That would simplify the parsing and eliminate the need to pass int[] arrays. If you want to reinstate the original behaviour, you can do so while still adhering to the interfaces laid out below.

I'm not a fan of the way startGame() can play multiple games. I'd factor out the "Play again?" loop and prompt.

When using a Scanner, it would be good to catch NoSuchElementException in case EOF is encountered.

### CursedNumberGame.java

import java.io.PrintStream;
import java.util.NoSuchElementException;
import java.util.Scanner;

public class CursedNumberGame {

public interface Player {
/**
* Given the parameters of the game (max and avoid), and the
* current sum, chooses a number between 1 and max inclusive.
*/
int play(int currentSum, int max, int avoid);
}

private final int maxPerTurn, avoid;

public CursedNumberGame(int maxPerTurn, int avoid) {
this.maxPerTurn = maxPerTurn;
this.avoid = avoid;
}

public static void displayHelp(PrintStream out) {
out.println("The goal of this game is to stay below the number 21.\n\n"
+ "At each turn, a player chooses either \"1\", \"2\", or \"3\".\n"
+ "That number will be added to the current number.\n\n"
+ "You will be playing AI, and you will start. Try your best, but no matter how\n"
+ "hard you try, you will lose!");
}

public void run(Scanner scanner, PrintStream out) {
displayHelp(out);

Player[] players = new Player[] { new HumanPlayer(scanner, out), new AI() };
int p, sum;
for (p = 0, sum = 0; sum < this.avoid; p = 1 - p) {
int choice = players[p].play(sum, this.maxPerTurn, this.avoid);
out.printf("%s played %d.  The sum is now %d.\n", players[p], choice, sum + choice);
sum += choice;
}
out.printf("%s lost!\n", players[1 - p]);
}

private static boolean doAgain(Scanner scanner, PrintStream out) {
out.print("Do you want to play again? ");
while (true) {
char in = Character.toUpperCase(scanner.nextLine().charAt(0));
if (in == 'Y') {
return true;
} else if (in == 'N') {
return false;
}
out.print("Oops, that was not valid input. Try again: ");
}
}

public static void main(String[] args) {
CursedNumberGame game = new CursedNumberGame(3, 21);
try (Scanner scanner = new Scanner(System.in)) {
do {
game.run(scanner, System.out);
} while (doAgain(scanner, System.out));
} catch (NoSuchElementException eof) {
}
System.out.println("Thanks for playing!");
}
}


### AI.java

public class AI implements CursedNumberGame.Player {
@Override
public int play(int currentSum, int max, int avoid) {
assert(max == 3);
assert(avoid == 21);
assert(4 - (currentSum % 4) < max);
return 4 - (currentSum % 4);
}

@Override
public String toString() {
return "The AI";
}
}


### HumanPlayer.java

import java.util.Scanner;
import java.io.PrintStream;

public class HumanPlayer implements CursedNumberGame.Player {
private static final String HELP = "help";

private final Scanner in;
private final PrintStream out;

public HumanPlayer(Scanner in, PrintStream out) {
this.in = in;
this.out = out;
}

@Override
public int play(int currentSum, int max, int avoid) {
out.printf("\nEnter a number from 1 to %d inclusive: ", max);
do {
String input = in.nextLine();
if (HELP.equals(input)) {
CursedNumberGame.displayHelp(out);
continue;
} else try {
int n = Integer.parseInt(input);
if (0 < n && n <= max) {
return n;
}
} catch (NumberFormatException notAnInt) {
}
out.print("Oops, illegal input. Try again: ");
} while (true);
}

@Override
public String toString() {
return "You";
}
}