/ assign3 / GuessNumber.java
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  }