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