1 package featurecat.lizzie; 2 3 import featurecat.lizzie.theme.Theme; 4 import java.awt.Color; 5 import java.io.*; 6 import java.nio.file.Files; 7 import java.nio.file.Paths; 8 import java.util.*; 9 import javax.swing.*; 10 import org.json.*; 11 12 public class Config { 13 public String language = "en"; 14 15 public boolean showBorder = false; 16 public boolean showMoveNumber = false; 17 public int onlyLastMoveNumber = 0; 18 // 0: Do not show; -1: Show all move number; other: Show last move number 19 public int allowMoveNumber = -1; 20 public boolean newMoveNumberInBranch = true; 21 public boolean showWinrate = true; 22 public boolean largeWinrate = false; 23 public boolean showBlunderBar = true; 24 public boolean weightedBlunderBarHeight = false; 25 public boolean dynamicWinrateGraphWidth = false; 26 public boolean showVariationGraph = true; 27 public boolean showComment = true; 28 public boolean showRawBoard = false; 29 public boolean showBestMovesTemporarily = false; 30 public boolean showCaptured = true; 31 public boolean handicapInsteadOfWinrate = false; 32 public boolean showDynamicKomi = true; 33 public double replayBranchIntervalSeconds = 1.0; 34 public boolean showCoordinates = false; 35 public boolean colorByWinrateInsteadOfVisits = false; 36 public boolean showLcbWinrate = false; 37 38 public boolean showStatus = true; 39 public boolean showBranch = true; 40 public boolean showBestMoves = true; 41 public boolean showNextMoves = true; 42 public boolean showSubBoard = true; 43 public boolean largeSubBoard = false; 44 public boolean startMaximized = true; 45 46 public JSONObject config; 47 public JSONObject leelazConfig; 48 public JSONObject uiConfig; 49 public JSONObject persisted; 50 public JSONObject persistedUi; 51 52 private Boolean macAppBundle = System.getenv().containsKey("MAC_APP_BUNDLE"); 53 private String configFilename = "config.txt"; 54 private String persistFilename = "persist"; 55 56 public Theme theme; 57 public float winrateStrokeWidth = 3; 58 public int minimumBlunderBarWidth = 3; 59 public int shadowSize = 100; 60 public String fontName = null; 61 public String uiFontName = null; 62 public String winrateFontName = null; 63 public int commentFontSize = 0; 64 public Color commentFontColor = null; 65 public Color commentBackgroundColor = null; 66 public Color winrateLineColor = null; 67 public Color winrateMissLineColor = null; 68 public Color blunderBarColor = null; 69 public boolean solidStoneIndicator = false; 70 public boolean showCommentNodeColor = true; 71 public Color commentNodeColor = null; 72 public Optional<List<Double>> blunderWinrateThresholds; 73 public Optional<Map<Double, Color>> blunderNodeColors; 74 public int nodeColorMode = 0; 75 public boolean appendWinrateToComment = true; 76 public int boardPositionProportion = 4; 77 public String gtpConsoleStyle = ""; 78 private final String defaultGtpConsoleStyle = 79 "body {background:#000000; color:#d0d0d0; font-family:Consolas, Menlo, Monaco, 'Ubuntu Mono', monospace; margin:4px;} .command {color:#ffffff;font-weight:bold;} .winrate {color:#ffffff;font-weight:bold;} .coord {color:#ffffff;font-weight:bold;}"; 80 loadAndMergeConfig( JSONObject defaultCfg, String fileName, boolean needValidation)81 private JSONObject loadAndMergeConfig( 82 JSONObject defaultCfg, String fileName, boolean needValidation) throws IOException { 83 File file = new File(fileName); 84 if (!file.canRead()) { 85 System.err.printf("Creating config file %s\n", fileName); 86 try { 87 writeConfig(defaultCfg, file); 88 } catch (JSONException e) { 89 e.printStackTrace(); 90 System.exit(1); 91 } 92 } 93 94 FileInputStream fp = new FileInputStream(file); 95 96 JSONObject mergedcfg = new JSONObject(new JSONTokener(fp)); 97 boolean modified = mergeDefaults(mergedcfg, defaultCfg); 98 99 fp.close(); 100 101 // Validate and correct settings 102 if (needValidation && validateAndCorrectSettings(mergedcfg)) { 103 modified = true; 104 } 105 106 if (modified) { 107 writeConfig(mergedcfg, file); 108 } 109 return mergedcfg; 110 } 111 112 /** 113 * Check settings to ensure its consistency, especially for those whose types are not <code> 114 * boolean</code>. If any inconsistency is found, try to correct it or to report it. <br> 115 * For example, we only support square boards of size >= 2x2. If the configured board size is not 116 * in the list above, we should correct it. 117 * 118 * @param config The config json object to check 119 * @return if any correction has been made. 120 */ validateAndCorrectSettings(JSONObject config)121 private boolean validateAndCorrectSettings(JSONObject config) { 122 boolean madeCorrections = false; 123 124 // Check ui configs 125 JSONObject ui = config.getJSONObject("ui"); 126 127 // Check board-size 128 int boardSize = ui.optInt("board-size", 19); 129 if (boardSize < 2) { 130 // Correct it to default 19x19 131 ui.put("board-size", 19); 132 madeCorrections = true; 133 } 134 135 // Check engine configs 136 JSONObject leelaz = config.getJSONObject("leelaz"); 137 // Checks for startup directory. It should exist and should be a directory. 138 String engineStartLocation = getBestDefaultLeelazPath(); 139 if (!(Files.exists(Paths.get(engineStartLocation)) 140 && Files.isDirectory(Paths.get(engineStartLocation)))) { 141 leelaz.put("engine-start-location", "."); 142 madeCorrections = true; 143 } 144 145 return madeCorrections; 146 } 147 Config()148 public Config() throws IOException { 149 JSONObject defaultConfig = createDefaultConfig(); 150 JSONObject persistConfig = createPersistConfig(); 151 152 // Main properties 153 this.config = loadAndMergeConfig(defaultConfig, configFilename, true); 154 // Persisted properties 155 this.persisted = loadAndMergeConfig(persistConfig, persistFilename, false); 156 157 leelazConfig = config.getJSONObject("leelaz"); 158 uiConfig = config.getJSONObject("ui"); 159 persistedUi = persisted.getJSONObject("ui-persist"); 160 161 theme = new Theme(uiConfig); 162 163 showBorder = uiConfig.optBoolean("show-border", false); 164 showMoveNumber = uiConfig.getBoolean("show-move-number"); 165 onlyLastMoveNumber = uiConfig.optInt("only-last-move-number"); 166 allowMoveNumber = showMoveNumber ? (onlyLastMoveNumber > 0 ? onlyLastMoveNumber : -1) : 0; 167 newMoveNumberInBranch = uiConfig.optBoolean("new-move-number-in-branch", true); 168 showStatus = uiConfig.getBoolean("show-status"); 169 showBranch = uiConfig.getBoolean("show-leelaz-variation"); 170 showWinrate = uiConfig.getBoolean("show-winrate"); 171 largeWinrate = uiConfig.optBoolean("large-winrate", false); 172 showBlunderBar = uiConfig.optBoolean("show-blunder-bar", true); 173 weightedBlunderBarHeight = uiConfig.optBoolean("weighted-blunder-bar-height", false); 174 dynamicWinrateGraphWidth = uiConfig.optBoolean("dynamic-winrate-graph-width", false); 175 showVariationGraph = uiConfig.getBoolean("show-variation-graph"); 176 showComment = uiConfig.optBoolean("show-comment", true); 177 showCaptured = uiConfig.getBoolean("show-captured"); 178 showBestMoves = uiConfig.getBoolean("show-best-moves"); 179 showNextMoves = uiConfig.getBoolean("show-next-moves"); 180 showSubBoard = uiConfig.getBoolean("show-subboard"); 181 largeSubBoard = uiConfig.getBoolean("large-subboard"); 182 handicapInsteadOfWinrate = uiConfig.getBoolean("handicap-instead-of-winrate"); 183 showDynamicKomi = uiConfig.getBoolean("show-dynamic-komi"); 184 appendWinrateToComment = uiConfig.optBoolean("append-winrate-to-comment"); 185 showCoordinates = uiConfig.optBoolean("show-coordinates"); 186 replayBranchIntervalSeconds = uiConfig.optDouble("replay-branch-interval-seconds", 1.0); 187 colorByWinrateInsteadOfVisits = uiConfig.optBoolean("color-by-winrate-instead-of-visits"); 188 boardPositionProportion = uiConfig.optInt("board-postion-proportion", 4); 189 winrateStrokeWidth = theme.winrateStrokeWidth(); 190 minimumBlunderBarWidth = theme.minimumBlunderBarWidth(); 191 shadowSize = theme.shadowSize(); 192 showLcbWinrate = config.getJSONObject("leelaz").getBoolean("show-lcb-winrate"); 193 194 if (theme.fontName() != null) fontName = theme.fontName(); 195 196 if (theme.uiFontName() != null) uiFontName = theme.uiFontName(); 197 198 if (theme.winrateFontName() != null) winrateFontName = theme.winrateFontName(); 199 200 commentFontSize = theme.commentFontSize(); 201 commentFontColor = theme.commentFontColor(); 202 commentBackgroundColor = theme.commentBackgroundColor(); 203 winrateLineColor = theme.winrateLineColor(); 204 winrateMissLineColor = theme.winrateMissLineColor(); 205 blunderBarColor = theme.blunderBarColor(); 206 solidStoneIndicator = theme.solidStoneIndicator(); 207 showCommentNodeColor = theme.showCommentNodeColor(); 208 commentNodeColor = theme.commentNodeColor(); 209 blunderWinrateThresholds = theme.blunderWinrateThresholds(); 210 blunderNodeColors = theme.blunderNodeColors(); 211 nodeColorMode = theme.nodeColorMode(); 212 213 gtpConsoleStyle = uiConfig.optString("gtp-console-style", defaultGtpConsoleStyle); 214 215 System.out.println(Locale.getDefault().getLanguage()); // todo add config option for language... 216 setLanguage(Locale.getDefault().getLanguage()); 217 } 218 219 // Modifies config by adding in values from default_config that are missing. 220 // Returns whether it added anything. mergeDefaults(JSONObject config, JSONObject defaultsConfig)221 public boolean mergeDefaults(JSONObject config, JSONObject defaultsConfig) { 222 boolean modified = false; 223 Iterator<String> keys = defaultsConfig.keys(); 224 while (keys.hasNext()) { 225 String key = keys.next(); 226 Object newVal = defaultsConfig.get(key); 227 if (newVal instanceof JSONObject) { 228 if (!config.has(key)) { 229 config.put(key, new JSONObject()); 230 modified = true; 231 } 232 Object oldVal = config.get(key); 233 modified |= mergeDefaults((JSONObject) oldVal, (JSONObject) newVal); 234 } else { 235 if (!config.has(key)) { 236 config.put(key, newVal); 237 modified = true; 238 } 239 } 240 } 241 return modified; 242 } 243 toggleShowMoveNumber()244 public void toggleShowMoveNumber() { 245 if (this.onlyLastMoveNumber > 0) { 246 allowMoveNumber = 247 (allowMoveNumber == -1 ? onlyLastMoveNumber : (allowMoveNumber == 0 ? -1 : 0)); 248 } else { 249 allowMoveNumber = (allowMoveNumber == 0 ? -1 : 0); 250 } 251 } 252 toggleNodeColorMode()253 public void toggleNodeColorMode() { 254 this.nodeColorMode = this.nodeColorMode > 1 ? 0 : this.nodeColorMode + 1; 255 } 256 toggleShowBranch()257 public void toggleShowBranch() { 258 this.showBranch = !this.showBranch; 259 } 260 toggleShowWinrate()261 public void toggleShowWinrate() { 262 this.showWinrate = !this.showWinrate; 263 } 264 toggleLargeWinrate()265 public void toggleLargeWinrate() { 266 this.largeWinrate = !this.largeWinrate; 267 } 268 toggleShowLcbWinrate()269 public void toggleShowLcbWinrate() { 270 this.showLcbWinrate = !this.showLcbWinrate; 271 } 272 toggleShowVariationGraph()273 public void toggleShowVariationGraph() { 274 this.showVariationGraph = !this.showVariationGraph; 275 } 276 toggleShowComment()277 public void toggleShowComment() { 278 this.showComment = !this.showComment; 279 } 280 toggleShowCommentNodeColor()281 public void toggleShowCommentNodeColor() { 282 this.showCommentNodeColor = !this.showCommentNodeColor; 283 } 284 toggleShowBestMoves()285 public void toggleShowBestMoves() { 286 this.showBestMoves = !this.showBestMoves; 287 } 288 toggleShowNextMoves()289 public void toggleShowNextMoves() { 290 this.showNextMoves = !this.showNextMoves; 291 } 292 toggleHandicapInsteadOfWinrate()293 public void toggleHandicapInsteadOfWinrate() { 294 this.handicapInsteadOfWinrate = !this.handicapInsteadOfWinrate; 295 } 296 toggleLargeSubBoard()297 public void toggleLargeSubBoard() { 298 this.largeSubBoard = !this.largeSubBoard; 299 } 300 toggleCoordinates()301 public void toggleCoordinates() { 302 showCoordinates = !showCoordinates; 303 } 304 toggleEvaluationColoring()305 public void toggleEvaluationColoring() { 306 colorByWinrateInsteadOfVisits = !colorByWinrateInsteadOfVisits; 307 } 308 showLargeSubBoard()309 public boolean showLargeSubBoard() { 310 return showSubBoard && largeSubBoard; 311 } 312 showLargeWinrate()313 public boolean showLargeWinrate() { 314 return showWinrate && largeWinrate; 315 } 316 showBestMovesNow()317 public boolean showBestMovesNow() { 318 return showBestMoves || showBestMovesTemporarily; 319 } 320 showBranchNow()321 public boolean showBranchNow() { 322 return showBranch || showBestMovesTemporarily; 323 } 324 325 /** 326 * Scans the current directory as well as the current PATH to find a reasonable default leelaz 327 * binary. 328 * 329 * @return A working path to a leelaz binary. If there are none on the PATH, "./leelaz" is 330 * returned for backwards compatibility. 331 */ getBestDefaultLeelazPath()332 public static String getBestDefaultLeelazPath() { 333 List<String> potentialPaths = new ArrayList<>(); 334 potentialPaths.add("."); 335 potentialPaths.addAll(Arrays.asList(System.getenv("PATH").split(":"))); 336 337 for (String potentialPath : potentialPaths) { 338 for (String potentialExtension : Arrays.asList(new String[] {"", ".exe"})) { 339 File potentialLeelaz = new File(potentialPath, "leelaz" + potentialExtension); 340 if (potentialLeelaz.exists() && potentialLeelaz.canExecute()) { 341 return potentialLeelaz.getPath(); 342 } 343 } 344 } 345 346 return "./leelaz"; 347 } 348 createDefaultConfig()349 private JSONObject createDefaultConfig() { 350 JSONObject config = new JSONObject(); 351 352 // About engine parameter 353 JSONObject leelaz = new JSONObject(); 354 leelaz.put("network-file", "network.gz"); 355 if (this.macAppBundle) { 356 // Mac Apps don't really expect the user to modify the current working directory, since that 357 // resides inside the app bundle. So a more sensible default in this context is to expect 358 // the user to already have a ~/.local/share/leela-zero/best-network file, which has been 359 // standard since Leela 0.16. 360 leelaz.put( 361 "engine-command", String.format("%s --gtp --lagbuffer 0", getBestDefaultLeelazPath())); 362 } else { 363 leelaz.put( 364 "engine-command", 365 String.format( 366 "%s --gtp --lagbuffer 0 --weights %%network-file", getBestDefaultLeelazPath())); 367 } 368 leelaz.put("engine-start-location", "."); 369 leelaz.put("max-analyze-time-minutes", 5); 370 leelaz.put("max-game-thinking-time-seconds", 2); 371 leelaz.put("print-comms", false); 372 leelaz.put("analyze-update-interval-centisec", 10); 373 leelaz.put("show-lcb-winrate", false); 374 375 config.put("leelaz", leelaz); 376 377 // About User Interface display 378 JSONObject ui = new JSONObject(); 379 380 ui.put("board-color", new JSONArray("[217, 152, 77]")); 381 ui.put("shadows-enabled", true); 382 ui.put("fancy-stones", true); 383 ui.put("fancy-board", true); 384 ui.put("shadow-size", 100); 385 ui.put("show-move-number", false); 386 ui.put("show-status", true); 387 ui.put("show-leelaz-variation", true); 388 ui.put("show-winrate", true); 389 ui.put("large-winrate", false); 390 ui.put("winrate-stroke-width", 3); 391 ui.put("show-blunder-bar", true); 392 ui.put("minimum-blunder-bar-width", 3); 393 ui.put("weighted-blunder-bar-height", false); 394 ui.put("dynamic-winrate-graph-width", false); 395 ui.put("show-comment", true); 396 ui.put("comment-font-size", 0); 397 ui.put("show-variation-graph", true); 398 ui.put("show-captured", true); 399 ui.put("show-best-moves", true); 400 ui.put("show-next-moves", true); 401 ui.put("show-subboard", true); 402 ui.put("large-subboard", false); 403 ui.put("win-rate-always-black", false); 404 ui.put("confirm-exit", false); 405 ui.put("resume-previous-game", false); 406 ui.put("autosave-interval-seconds", -1); 407 ui.put("handicap-instead-of-winrate", false); 408 ui.put("board-size", 19); 409 ui.put("show-dynamic-komi", true); 410 ui.put("min-playout-ratio-for-stats", 0.0); 411 ui.put("theme", "default"); 412 ui.put("only-last-move-number", 0); 413 ui.put("new-move-number-in-branch", true); 414 ui.put("append-winrate-to-comment", false); 415 ui.put("replay-branch-interval-seconds", 1.0); 416 ui.put("gtp-console-style", defaultGtpConsoleStyle); 417 config.put("ui", ui); 418 return config; 419 } 420 createPersistConfig()421 private JSONObject createPersistConfig() { 422 JSONObject config = new JSONObject(); 423 424 // About engine parameter 425 JSONObject filesys = new JSONObject(); 426 filesys.put("last-folder", ""); 427 428 config.put("filesystem", filesys); 429 430 // About autosave 431 config.put("autosave", ""); 432 433 // About User Interface display 434 JSONObject ui = new JSONObject(); 435 436 // ui.put("window-height", 657); 437 // ui.put("window-width", 687); 438 // ui.put("max-alpha", 240); 439 440 // Main Window Position & Size 441 ui.put("main-window-position", new JSONArray("[]")); 442 ui.put("gtp-console-position", new JSONArray("[]")); 443 ui.put("window-maximized", false); 444 445 config.put("filesystem", filesys); 446 447 // Avoid the key "ui" because it was used to distinguish "config" and "persist" 448 // in old version of validateAndCorrectSettings(). 449 // If we use "ui" here, we will have trouble to run old lizzie. 450 config.put("ui-persist", ui); 451 return config; 452 } 453 writeConfig(JSONObject config, File file)454 private void writeConfig(JSONObject config, File file) throws IOException, JSONException { 455 file.createNewFile(); 456 457 FileOutputStream fp = new FileOutputStream(file); 458 OutputStreamWriter writer = new OutputStreamWriter(fp); 459 460 writer.write(config.toString(2)); 461 462 writer.close(); 463 fp.close(); 464 } 465 persist()466 public void persist() throws IOException { 467 boolean windowIsMaximized = Lizzie.frame.getExtendedState() == JFrame.MAXIMIZED_BOTH; 468 469 JSONArray mainPos = new JSONArray(); 470 if (!windowIsMaximized) { 471 mainPos.put(Lizzie.frame.getX()); 472 mainPos.put(Lizzie.frame.getY()); 473 mainPos.put(Lizzie.frame.getWidth()); 474 mainPos.put(Lizzie.frame.getHeight()); 475 } 476 persistedUi.put("main-window-position", mainPos); 477 JSONArray gtpPos = new JSONArray(); 478 gtpPos.put(Lizzie.gtpConsole.getX()); 479 gtpPos.put(Lizzie.gtpConsole.getY()); 480 gtpPos.put(Lizzie.gtpConsole.getWidth()); 481 gtpPos.put(Lizzie.gtpConsole.getHeight()); 482 persistedUi.put("gtp-console-position", gtpPos); 483 persistedUi.put("board-postion-propotion", Lizzie.frame.BoardPositionProportion); 484 persistedUi.put("window-maximized", windowIsMaximized); 485 writeConfig(this.persisted, new File(persistFilename)); 486 } 487 save()488 public void save() throws IOException { 489 writeConfig(this.config, new File(configFilename)); 490 } 491 setLanguage(String code)492 public void setLanguage(String code) { 493 // currently will not set the resource bundle. TODO. 494 if (code.equals("ko")) { 495 // korean 496 if (fontName == null) { 497 fontName = "Malgun Gothic"; 498 } 499 if (uiFontName == null) { 500 uiFontName = "Malgun Gothic"; 501 } 502 winrateFontName = null; 503 } 504 } 505 } 506