GuessNumber.java
1 /* 2 * Guess a number, either seeded as argument or chosen at random 3 4 Emits regular output to stdout, and debug hints to stderr 5 6 Requires JDK 17+ 7 8 TODO: improve input validation 9 TODO: generalize string-to-number parsing and validation 10 as reusable function for both seed and guess 11 TODO: add class Agent and support naming game master and player, 12 extendable for multiple players guessing in turns 13 14 * SPDX-FileCopyrightText: 2024 Jonas Smedegaard <dr@jones.dk 15 * 16 * SPDX-License-Identifier: GPL-3.0-or-later 17 */ 18 19 import java.util.Optional; 20 import java.util.Scanner; 21 import java.util.InputMismatchException; 22 23 // requires JDK 7+ but offers an ergonomic iterator 24 // (is also thread-safe albeit irrelevant here) 25 // source: <https://stackoverflow.com/a/363692/18619283> 26 import java.util.concurrent.ThreadLocalRandom; 27 28 public class GuessNumber { 29 public static void main(String[] args) { 30 SimpleLog log = new SimpleLog(); 31 32 // parse options from command-line arguments 33 SimpleGetopt opts = new SimpleGetopt(log, args); 34 35 // set log level 36 log.level(opts.log_level); 37 38 // instantiate object containing a number, optionally seeded 39 GuessableNumber secret; 40 String gameMaster; 41 if (opts.seed == null) { 42 secret = new GuessableNumber(log); 43 gameMaster = "I"; 44 } else { 45 secret = new GuessableNumber(log, opts.seed); 46 gameMaster = "You"; 47 } 48 49 // introduce game 50 System.out.printf( 51 "%s have chosen a number between 1 and 100. Try to guess it!\n", 52 gameMaster 53 ); 54 55 // REPL loop until correct number is guessed 56 // * hit CTRL+c to give up 57 // * when done/interrupted, number is revealed to stderr 58 while (true) { 59 switch (secret.guess()) { 60 case -1: 61 System.out.println("The number is lower!"); 62 break; 63 case 0: 64 System.out.println("Correct!"); 65 System.exit(0); 66 case 1: 67 System.out.println("The number is higher!"); 68 break; 69 } 70 } 71 } 72 } 73 74 // conceil the number to guess in an object 75 class GuessableNumber { 76 77 // instantiate with seed value 78 public GuessableNumber(SimpleLog log, String seedInput) { 79 this.log = log; 80 log.debug("received number: %s", seedInput); 81 Long seedNumber = Long.valueOf(seedInput); 82 this.number = Integer.valueOf(seedInput); 83 84 revealAtDestruction(); 85 } 86 87 // instantiate without seed value 88 public GuessableNumber(SimpleLog log) { 89 this.log = log; 90 91 // get pseudorandom integer in range 1-100 (both inclusively) 92 // (iterator nextInt() upper bound is exclusive) 93 this.number = ThreadLocalRandom.current().nextInt(1, 101); 94 95 revealAtDestruction(); 96 } 97 98 private final SimpleLog log; 99 100 // * a byte would be adequate for the secret number itself 101 // (is within range of the 128 positive bits), 102 // but an int is more ergonomic: easier to cast 103 // * lock down number when defined 104 private final int number; 105 106 // instantiate integer scanner 107 // * sloppily treat any non-integer as delimiting noise 108 // (robust, but scanner "hangs" until a number is typed) 109 private Scanner scanner = new Scanner(System.in).useDelimiter("[^\\d]*[^\\d-]"); 110 111 public int guess() { 112 113 // emit a prompt 114 System.out.print("Please enter a number -> "); 115 116 // gracefully handle stupidly out-of-range input 117 int guess; 118 while (true) { 119 try { 120 guess = this.scanner.nextInt(); 121 if (guess != 0) { 122 break; 123 } 124 } catch (InputMismatchException e) { 125 this.log.warning("Not a number or out of range, please try again!"); 126 scanner.next(); // flush buffer 127 } 128 } 129 130 // reveal direction of signed distance from secret 131 return Integer.compare(this.number - guess, 0); 132 } 133 134 // emit secret to stderr if interrupted, as a debug aid 135 private void revealAtDestruction() { 136 // call log object only outside of hook to avoid hanging (race?) 137 if (log.level >= log.DEBUG) { 138 Runtime.getRuntime().addShutdownHook(new Thread(() -> { 139 System.err.printf("\n(the number was %s)\n", number); 140 })); 141 } 142 } 143 } 144 145 // inline option parser 146 // (inline: java is competitive at the core -> discourages code sharing) 147 // listed as first class to get the help message close to the top 148 class SimpleGetopt { 149 150 public SimpleGetopt(SimpleLog log, String[] args) { 151 for (int i = 0; i < args.length; i++) { 152 switch (args[i]) { 153 case "--help": 154 System.out.println(help); 155 System.exit(0); 156 break; 157 case "--debug": 158 this.log_level = log.DEBUG; 159 break; 160 case "--seed": 161 i++; 162 if (i < args.length) { 163 this.seed = args[i]; 164 break; 165 } 166 System.err.printf("option '--seed' requires an argument\n\n"); 167 System.err.println(help); 168 System.exit(1); 169 default: 170 System.err.printf("Unknown option: %s\n\n", args[i]); 171 System.err.println(help); 172 System.exit(1); 173 } 174 } 175 } 176 177 // requires JDK 17+ but is more ergonomic than concatenating 178 // source: <https://stackoverflow.com/a/50155171/18619283> 179 String help = """ 180 Usage: GuessNumber [OPTION]... 181 Interactive game to guess a number. 182 The number is either seeded as argument or chosen at random. 183 184 --seed NUMBER seed the number to guess 185 (default: use a random number) 186 --debug enable debug messages 187 --help this message 188 """; 189 190 // TODO: avoid magic number (maybe needs casting constant to variable?) 191 // int log_level = SimpleLog.INFO; 192 int log_level = 3; 193 194 String seed = null; 195 } 196 197 // simple inline reusable log handler 198 // (inline: java is competitive at the core -> discourages code sharing) 199 // TODO: cover all input types for all log levels 200 // TODO: use factory builder to reduce duplicate code 201 class SimpleLog { 202 public SimpleLog() {} 203 204 public final int FATAL = 0; 205 public final int ERR = 1; 206 public final int WARN = 2; 207 public final int INFO = 3; 208 public final int DEBUG = 4; 209 public final int TRACE = 5; 210 211 public int level; 212 213 public int level(int level) { 214 this.level = level; 215 216 return level; 217 } 218 219 // accept either a string or printf pattern + string/int 220 // TODO: maybe instead accept pattern + string array 221 public void fatal(String msg) { 222 if (level >= this.FATAL) { 223 System.err.printf("ERROR: %s\n", msg); 224 } 225 System.exit(1); 226 } 227 public void fatal(String pattern, String msg) { 228 if (level >= this.FATAL) { 229 System.err.printf("ERROR: " + pattern + "\n", msg); 230 } 231 System.exit(1); 232 } 233 public void fatal(String pattern, Integer i) { 234 if (level >= this.FATAL) { 235 System.err.printf("ERROR: " + pattern + "\n", i); 236 } 237 System.exit(1); 238 } 239 240 public void warning(String msg) { 241 if (level >= this.WARN) { 242 System.err.printf("WARNING: %s\n", msg); 243 } 244 } 245 246 public void debug(String msg) { 247 if (level >= this.DEBUG) { 248 System.err.printf("DEBUG: %s\n", msg); 249 } 250 } 251 public void debug(String pattern, String msg) { 252 if (level >= this.DEBUG) { 253 System.err.printf("DEBUG: " + pattern + "\n", msg); 254 } 255 } 256 }