diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index e23f350..4989a61 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,7 +1,7 @@
# These are supported funding model platforms
github: sanderland # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
-patreon: katrainbaduk # Replace with a single Patreon username
+patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
diff --git a/.gitignore b/.gitignore
index ea3ff0e..031afa5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ tmp.pickle
my
logs
callgrind.*
+cpp/out
+.vs
# debug
outdated_log.txt
diff --git a/CONTRIBUTIONS.md b/CONTRIBUTIONS.md
index 9b314ca..8220031 100644
--- a/CONTRIBUTIONS.md
+++ b/CONTRIBUTIONS.md
@@ -52,6 +52,7 @@ Many thanks to these additional authors:
* "fohristiwhirl" for the Gibo and NGF formats parsing code.
* "kaorahi" for bug fixes and SGF parser improvements.
* "ajkenny84" for the red-green colourblind theme.
+* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
## Translators
@@ -62,7 +63,7 @@ Many thanks to the following contributors for translations.
* German: "nimets123" with contributions from "trohde"
* Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community
* Russian: Dmitry Ivankov and Alexander Kiselev
-* Simplified Chinese: Qing Mu with contributions from "Medwin"
+* Simplified Chinese: Qing Mu with contributions from "Medwin" and Viktor Lin
* Japanese: "kaorahi"
## Additional thanks to
diff --git a/INSTALL.md b/INSTALL.md
index f5ba1d4..e2a52fd 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -53,7 +53,7 @@ Simply download and run, everything is included.
### Quick install guide
-If you have a working Python 3.6/3.7 available, you should be able to simply:
+If you have a working Python 3.6/3.7/3.8 available, you should be able to simply:
* Run `pip3 install -U katrain`
* Run the program by executing `katrain` in a terminal.
diff --git a/LICENSE b/LICENSE
index 0d8b8aa..c972dd9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,10 +1,11 @@
This repository includes:
-1. Binaries for 'KataGo', which is Copyright 2019 David J Wu et al.
+1. Binaries for 'KataGo', which is Copyright David J Wu et al.
For on related licenses for these binaries and libraries see https://github.com/lightvector/KataGo
2. Icons from www.flaticon.com, used with permission with the following attributions:
- Equalize icon and Thrash Icon: derived from work by bqlqn from www.flaticon.com
+- Local search icon: derived from work by Pixel Perfect from www.flaticon.com
- Other Menu icons and Flag icons: derived from work by Freepik from www.flaticon.com
3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use.
diff --git a/README.md b/README.md
index 634cc85..ee5b446 100644
--- a/README.md
+++ b/README.md
@@ -120,6 +120,7 @@ This estimate should be reasonably accurate as long as you have not changed the
* **Calibrated Rank Bot** was calibrated on various bots (e.g. GnuGo and Pachi at different strength settings) to play a balanced
game from the opening to the endgame without making serious (DDK) blunders. Further discussion can be found
[here](https://github.com/sanderland/katrain/issues/44) and [here](https://github.com/sanderland/katrain/issues/74).
+ * **Simple Style** Prefers moves that solidify both player's territory, leading to relatively simpler moves.
* **ScoreLoss** is KataGo analyzing as usual, but
choosing from potential moves depending on the expected score loss, leading to a varied style with mostly small mistakes.
* **Policy** uses the top move from the policy network (it's 'shape sense' without reading).
@@ -166,6 +167,7 @@ Keyboard shortcuts are shown with **[key]**.
* **[s]**: Equalize visits: Re-evaluate all currently shown next moves with the same visits as the current top move. Useful to increase confidence in the suggestions with high uncertainty.
* **[d]**: Analyze all moves: Evaluate all possible next moves. This can take a bit of time even though 'fast_visits' is used, but can be useful to see how many reasonable next moves are available.
* **[f]**: Find alternatives: Increases analysis of current candidate moves to at least the 'fast visits' level, and request a new query that excludes all current candidate moves.
+ * **[g]**: Find local responses: Searches within 5 spaces of the last move only.
* **[spacebar]**: Turn continuous analysis on/off. This will continuously improve analysis of the current position, similar to Lizzie's 'pondering', but only when there are no other queries going on.
* **[enter]** AI move. Makes the AI move for the current player regardless of current player selection.
@@ -180,6 +182,8 @@ In addition to shortcuts mentioned above and those shown in the main menu:
* **[arrow left]** or **[z]**: Undo move. Hold alt for 10 moves at a time, or ctrl to skip to the start.
* **[arrow right]** or **[x]**: Redo move. Hold alt for 10 moves at a time, or ctrl to skip to the start.
* **[arrow up/down]** Switch branch, as would be expected from the move tree.
+* **[n]** As in clicking the red arrow, go to one move before the next mistake (orange or worse) by a human player.
+* **[home/end]** Go to the beginning/end of the game.
* **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
* **[scroll down]**: Redo move. Only works when hovering the cursor over the board.
* **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
diff --git a/i18n.py b/i18n.py
index b1ca000..37f1dc0 100644
--- a/i18n.py
+++ b/i18n.py
@@ -26,7 +26,7 @@ for lang in locales:
pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po")
po[lang] = polib.pofile(pofile[lang])
for entry in po[lang].translated_entries():
- if "TODO" in entry.comment:
+ if "TODO" in entry.comment and not "DEPRECATED" in entry.comment:
todos[lang].append(entry)
strings_to_langs[entry.msgid][lang] = entry
strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
diff --git a/katrain/KataGo/analysis_config.cfg b/katrain/KataGo/analysis_config.cfg
index 84a0130..3386251 100644
--- a/katrain/KataGo/analysis_config.cfg
+++ b/katrain/KataGo/analysis_config.cfg
@@ -109,6 +109,7 @@ nnMaxBatchSize = 96
# Other General GPU Settings-------------------------------------------------------------------------------
+
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
nnCacheSizePowerOfTwo = 20
# Size of mutex pool for nnCache is 2 ** this
@@ -116,7 +117,6 @@ nnMutexPoolSizePowerOfTwo = 16
# Randomize board orientation when running neural net evals?
nnRandomize = true
-
# TO USE MULTIPLE GPUS:
# Set this to the number of GPUs you have and/or would like to use...
# AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
diff --git a/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll b/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll
deleted file mode 100644
index 2d6950d..0000000
Binary files a/katrain/KataGo/boost_filesystem-vc142-mt-x64-1_72.dll and /dev/null differ
diff --git a/katrain/KataGo/katago b/katrain/KataGo/katago
index 99298c0..a85dce4 100755
Binary files a/katrain/KataGo/katago and b/katrain/KataGo/katago differ
diff --git a/katrain/KataGo/katago.exe b/katrain/KataGo/katago.exe
index b416e3c..f65582a 100644
Binary files a/katrain/KataGo/katago.exe and b/katrain/KataGo/katago.exe differ
diff --git a/katrain/__main__.py b/katrain/__main__.py
index 502859e..e98a94a 100644
--- a/katrain/__main__.py
+++ b/katrain/__main__.py
@@ -30,11 +30,13 @@ Config.set("graphics", "width", max(400, int(WINDOW_X * WINDOW_SCALE_FAC)))
Config.set("graphics", "height", max(400, int(WINDOW_Y * WINDOW_SCALE_FAC)))
Config.set("input", "mouse", "mouse,multitouch_on_demand")
+import re
import signal
import sys
import threading
import traceback
from queue import Queue
+import urllib3
import webbrowser
from kivy.base import ExceptionHandler, ExceptionManager
@@ -130,9 +132,9 @@ class KaTrainGui(Screen, KaTrainBase):
return self.play_mode.mode
def toggle_continuous_analysis(self):
- self.idle_analysis = not self.idle_analysis
- if not self.idle_analysis:
+ if self.idle_analysis:
self.controls.set_status("", STATUS_INFO)
+ self.idle_analysis = not self.idle_analysis
self.update_state()
def start(self):
@@ -157,7 +159,6 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_controls.mid_circles_container.clear_widgets()
self.board_controls.mid_circles_container.add_widget(bot)
self.board_controls.mid_circles_container.add_widget(top)
- self.board_controls.branch.disabled = not cn.parent or len(cn.parent.children) <= 1
self.controls.players["W"].captures = prisoners["W"]
self.controls.players["B"].captures = prisoners["B"]
@@ -168,6 +169,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_controls.engine_status_col = ENGINE_READY_COL
else:
self.board_controls.engine_status_col = ENGINE_BUSY_COL
+ self.board_controls.queries_remaining = len(self.engine.queries)
# redraw board/stones
if redraw_board:
@@ -193,21 +195,21 @@ class KaTrainGui(Screen, KaTrainBase):
teaching_undo = cn.player and last_player.being_taught and cn.parent
if (
teaching_undo
- and cn.analysis_ready
- and cn.parent.analysis_ready
+ and cn.analysis_complete
+ and cn.parent.analysis_complete
and not cn.children
and not self.game.end_result
):
self.game.analyze_undo(cn) # not via message loop
if (
- cn.analysis_ready
+ cn.analysis_complete
and next_player.ai
and not cn.children
and not self.game.end_result
and not (teaching_undo and cn.auto_undo is None)
): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
self._do_ai_move(cn)
- Clock.schedule_once(self.board_gui.play_stone_sound, 0)
+ Clock.schedule_once(self.board_gui.play_stone_sound, 0.25)
if len(self.engine.queries) == 0 and self.idle_analysis:
self("analyze-extra", "extra", continuous=True)
Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger?
@@ -218,7 +220,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.update_players()
self.update_state()
for player_setup_block in PlayerSetupBlock.INSTANCES:
- player_setup_block.update_players(bw, self.players_info[bw])
+ player_setup_block.update_player_info(bw, self.players_info[bw])
def set_note(self, note):
self.game.current_node.note = note
@@ -293,6 +295,10 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_gui.animating_pv = None
self.game.redo(n_times)
+ def _do_next_mistake(self):
+ self.board_gui.animating_pv = None
+ self.game.redo(999, stop_on_mistake=self.config("trainer/eval_thresholds")[-4])
+
def _do_cycle_children(self, *args):
self.board_gui.animating_pv = None
self.game.cycle_children(*args)
@@ -324,7 +330,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True
if not self.timer_settings_popup:
self.timer_settings_popup = I18NPopup(
- title_key="timer settings", size=[dp(350), dp(400)], content=ConfigTimerPopup(self)
+ title_key="timer settings", size=[dp(600), dp(500)], content=ConfigTimerPopup(self)
).__self__
self.timer_settings_popup.content.popup = self.timer_settings_popup
self.timer_settings_popup.open()
@@ -351,12 +357,12 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True
if not self.ai_settings_popup:
self.ai_settings_popup = I18NPopup(
- title_key="ai settings", size=[dp(600), dp(650)], content=ConfigAIPopup(self)
+ title_key="ai settings", size=[dp(750), dp(750)], content=ConfigAIPopup(self)
).__self__
self.ai_settings_popup.content.popup = self.ai_settings_popup
self.ai_settings_popup.open()
- def load_sgf_file(self, file, fast=False, rewind=False):
+ def load_sgf_file(self, file, fast=False, rewind=True):
try:
move_tree = KaTrainSGF.parse_file(file)
except ParseError as e:
@@ -388,6 +394,7 @@ class KaTrainGui(Screen, KaTrainBase):
popup_contents.filesel.on_success = readfile
popup_contents.filesel.on_submit = readfile
self.fileselect_popup.open()
+ self.fileselect_popup.content.filesel.ids.list_view._trigger_update()
def _do_output_sgf(self):
msg = self.game.write_sgf(self.config("general/sgf_save"))
@@ -399,6 +406,14 @@ class KaTrainGui(Screen, KaTrainBase):
if not clipboard:
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_INFO)
return
+
+ url_match = re.match(r"(?Phttps?://[^\s]+)", clipboard)
+ if url_match:
+ self.log("Recognized url: " + url_match.group(), OUTPUT_INFO)
+ http = urllib3.PoolManager()
+ response = http.request("GET", url_match.group())
+ clipboard = response.data.decode("utf-8")
+
try:
move_tree = KaTrainSGF.parse_sgf(clipboard)
except Exception as exc:
@@ -440,7 +455,9 @@ class KaTrainGui(Screen, KaTrainBase):
"s": ("analyze-extra", "equalize"),
"d": ("analyze-extra", "sweep"),
"f": ("analyze-extra", "alternative"),
+ "g": ("analyze-extra", "local"),
"p": ("play", None),
+ "n": ("next-mistake",),
"down": ("switch-branch", 1),
"up": ("switch-branch", -1),
"f5": ("timer-popup",),
@@ -466,7 +483,9 @@ class KaTrainGui(Screen, KaTrainBase):
return
else:
return
-
+ ctrl_pressed = "ctrl" in modifiers
+ alt_pressed = "alt" in modifiers
+ shift_pressed = "shift" in modifiers
shortcuts = self.shortcuts
if keycode[1] == "tab":
self.play_mode.switch_ui_mode()
@@ -474,26 +493,30 @@ class KaTrainGui(Screen, KaTrainBase):
self.nav_drawer.set_state("toggle")
elif keycode[1] == "spacebar":
self.toggle_continuous_analysis()
- elif keycode[1] == "b" and "ctrl" not in modifiers:
+ elif keycode[1] == "b" and ctrl_pressed:
self.controls.timer.paused = not self.controls.timer.paused
- elif keycode[1] in ["`", "~", "m"] and "ctrl" not in modifiers:
+ elif keycode[1] in ["`", "~", "m"] and ctrl_pressed:
self.zen = (self.zen + 1) % 3
elif keycode[1] in ["left", "z"]:
- self("undo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
+ self("undo", 1 + (alt_pressed or shift_pressed) * 9 + (ctrl_pressed and not alt_pressed) * 999)
elif keycode[1] in ["right", "x"]:
- self("redo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
- elif keycode[1] == "n" and "ctrl" in modifiers:
+ self("redo", 1 + (alt_pressed or shift_pressed) * 9 + (ctrl_pressed and not alt_pressed) * 999)
+ elif keycode[1] == "home":
+ self("undo", 999)
+ elif keycode[1] == "end":
+ self("redo", 999)
+ elif keycode[1] == "n" and ctrl_pressed:
self("new-game-popup")
- elif keycode[1] == "l" and "ctrl" in modifiers:
+ elif keycode[1] == "l" and ctrl_pressed:
self("analyze-sgf-popup")
- elif keycode[1] == "s" and "ctrl" in modifiers:
+ elif keycode[1] == "s" and ctrl_pressed:
self("output-sgf")
- elif keycode[1] == "c" and "ctrl" in modifiers:
+ elif keycode[1] == "c" and ctrl_pressed:
Clipboard.copy(self.game.root.sgf())
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO)
- elif keycode[1] == "v" and "ctrl" in modifiers:
+ elif keycode[1] == "v" and ctrl_pressed:
self.load_sgf_from_clipboard()
- elif keycode[1] in shortcuts.keys() and "ctrl" not in modifiers:
+ elif keycode[1] in shortcuts.keys() and not ctrl_pressed:
shortcut = shortcuts[keycode[1]]
if isinstance(shortcut, Widget):
shortcut.trigger_action(duration=0)
diff --git a/katrain/config.json b/katrain/config.json
index a8e5203..7e7ee35 100644
--- a/katrain/config.json
+++ b/katrain/config.json
@@ -17,11 +17,13 @@
"anim_pv_time": 0.5,
"debug_level": 0,
"lang": "en",
- "version": "1.5.0"
+ "version": "1.6.1"
},
"timer": {
"byo_length": 30,
"byo_periods": 5,
+ "minimal_use": 0,
+ "main_time": 0,
"sound": true
},
"game": {
@@ -86,6 +88,22 @@
"ai:policy": {
"opening_moves": 22.0
},
+ "ai:simple": {
+ "max_points_lost": 1.75,
+ "settled_weight": 1.0,
+ "opponent_fac": 0.5,
+ "min_visits": 3,
+ "attach_penalty": 1,
+ "tenuki_penalty": 0.5
+ },
+ "ai:settle": {
+ "max_points_lost": 1.75,
+ "settled_weight": 1.0,
+ "opponent_fac": 0.5,
+ "min_visits": 3,
+ "attach_penalty": 1,
+ "tenuki_penalty": 0.5
+ },
"ai:p:weighted": {
"weaken_fac": 1.25,
"pick_override": 1.0,
diff --git a/katrain/core/ai.py b/katrain/core/ai.py
index e39923a..b15a1d6 100644
--- a/katrain/core/ai.py
+++ b/katrain/core/ai.py
@@ -8,29 +8,31 @@ from katrain.core.constants import (
AI_DEFAULT,
AI_HANDICAP,
AI_INFLUENCE,
+ AI_INFLUENCE_ELO_GRID,
AI_JIGO,
AI_LOCAL,
+ AI_LOCAL_ELO_GRID,
AI_PICK,
+ AI_PICK_ELO_GRID,
AI_POLICY,
AI_RANK,
AI_SCORELOSS,
+ AI_SCORELOSS_ELO,
+ AI_SIMPLE_OWNERSHIP,
AI_STRATEGIES_PICK,
AI_STRATEGIES_POLICY,
AI_STRENGTH,
AI_TENUKI,
+ AI_TENUKI_ELO_GRID,
AI_TERRITORY,
+ AI_TERRITORY_ELO_GRID,
AI_WEIGHTED,
+ AI_WEIGHTED_ELO,
+ CALIBRATED_RANK_ELO,
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_INFO,
- AI_WEIGHTED_ELO,
- AI_SCORELOSS_ELO,
- CALIBRATED_RANK_ELO,
- AI_LOCAL_ELO_GRID,
- AI_TENUKI_ELO_GRID,
- AI_TERRITORY_ELO_GRID,
- AI_INFLUENCE_ELO_GRID,
- AI_PICK_ELO_GRID,
+ AI_SETTLE_STONES,
)
from katrain.core.game import Game, GameNode, Move
from katrain.core.utils import var_to_grid
@@ -162,13 +164,14 @@ def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optio
error = False
analysis = None
- def set_analysis(a):
+ def set_analysis(a, partial_result):
nonlocal analysis
- analysis = a
+ if not partial_result:
+ analysis = a
def set_error(a):
nonlocal error
- game.katrain.log("Error in PDA-based analysis", a)
+ game.katrain.log(f"Error in additional analysis query: {a}")
error = True
engine = game.engines[cn.player]
@@ -203,7 +206,7 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT
- while not cn.analysis_ready:
+ while not cn.analysis_complete:
time.sleep(0.01)
game.engines[cn.next_player].check_alive(exception_if_dead=True)
@@ -328,7 +331,10 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
candidate_ai_moves = handicap_analysis["moveInfos"]
top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
- if top_cand.is_pass and ai_mode not in [AI_DEFAULT, AI_HANDICAP]: # don't play suicidal to balance score
+ if top_cand.is_pass and ai_mode not in [
+ AI_DEFAULT,
+ AI_HANDICAP,
+ ]: # don't play suicidal to balance score
aimove = top_cand
ai_thoughts += f"Top move is pass, so passing regardless of strategy. "
else:
@@ -352,6 +358,80 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
topmove = weighted_selection_without_replacement(moves, 1)[0]
aimove = topmove[2]
ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights."
+ elif ai_mode in [AI_SIMPLE_OWNERSHIP, AI_SETTLE_STONES]:
+ stones_with_player = {(*s.coords, s.player) for s in game.stones}
+ next_player_sign = cn.player_sign(cn.next_player)
+ if ai_mode == AI_SIMPLE_OWNERSHIP:
+
+ def settledness(d, player_sign, player):
+ return sum([abs(o) for o in d["ownership"] if player_sign * o > 0])
+
+ else:
+ board_size_x, board_size_y = game.board_size
+
+ def settledness(d, player_sign, player):
+ ownership_grid = var_to_grid(d["ownership"], (board_size_x, board_size_y))
+ return sum(
+ [abs(ownership_grid[s.coords[0]][s.coords[1]]) for s in game.stones if s.player == player]
+ )
+
+ def is_attachment(move):
+ if move.is_pass:
+ return False
+ attach_opponent_stones = sum(
+ (move.coords[0] + dx, move.coords[1] + dy, cn.player) in stones_with_player
+ for dx in [-1, 0, 1]
+ for dy in [-1, 0, 1]
+ if abs(dx) + abs(dy) == 1
+ )
+ nearby_own_stones = sum(
+ (move.coords[0] + dx, move.coords[1] + dy, cn.next_player) in stones_with_player
+ for dx in [-2, 0, 1, 2]
+ for dy in [-2 - 1, 0, 1, 2]
+ if abs(dx) + abs(dy) <= 2 # allows clamps/jumps
+ )
+ return attach_opponent_stones >= 1 and nearby_own_stones == 0
+
+ def is_tenuki(d):
+ return not d.is_pass and not any(
+ not node
+ or not node.move
+ or node.move.is_pass
+ or max(abs(last_c - cand_c) for last_c, cand_c in zip(node.move.coords, d.coords)) < 5
+ for node in [cn, cn.parent]
+ )
+
+ moves_with_settledness = sorted(
+ [
+ (
+ move,
+ settledness(d, next_player_sign, cn.next_player),
+ settledness(d, -next_player_sign, cn.player),
+ is_attachment(move),
+ is_tenuki(move),
+ d,
+ )
+ for d in candidate_ai_moves
+ if d["pointsLost"] < ai_settings["max_points_lost"]
+ and "ownership" in d
+ and (d["order"] <= 1 or d["visits"] >= ai_settings.get("min_visits", 1))
+ for move in [Move.from_gtp(d["move"], player=cn.next_player)]
+ if not (move.is_pass and d["pointsLost"] > 0.75)
+ ],
+ key=lambda t: t[5]["pointsLost"]
+ + ai_settings["attach_penalty"] * t[3]
+ + ai_settings["tenuki_penalty"] * t[4]
+ - ai_settings["settled_weight"] * (t[1] + ai_settings["opponent_fac"] * t[2]),
+ )
+ if moves_with_settledness:
+ cands = [
+ f"{move.gtp()} ({d['pointsLost']:.1f} pt lost, {d['visits']} visits, {settled:.1f} settledness, {oppsettled:.1f} opponent settledness{', attachment' if isattach else ''}{', tenuki' if istenuki else ''})"
+ for move, settled, oppsettled, isattach, istenuki, d in moves_with_settledness[:5]
+ ]
+ ai_thoughts += f"{ai_mode} strategy. Top 5 Candidates {', '.join(cands)} "
+ aimove = moves_with_settledness[0][0]
+ else:
+ raise (Exception("No moves found - are you using an older KataGo with no per-move ownership info?"))
else:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP]:
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
diff --git a/katrain/core/base_katrain.py b/katrain/core/base_katrain.py
index e8604a8..819a30c 100644
--- a/katrain/core/base_katrain.py
+++ b/katrain/core/base_katrain.py
@@ -49,13 +49,13 @@ class KaTrainBase:
"""Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI"""
- def __init__(self, force_package_config=False, debug_level=0, **kwargs):
- self.debug_level = debug_level
+ def __init__(self, force_package_config=False, debug_level=None, **kwargs):
+ self.debug_level = debug_level or 0
self.game = None
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
self.config_file = self._load_config(force_package_config=force_package_config)
- self.debug_level = debug_level or self.config("general/debug_level", OUTPUT_INFO)
+ self.debug_level = self.config("general/debug_level", OUTPUT_INFO) if debug_level is None else debug_level
Config.set("kivy", "log_level", "warning")
if self.debug_level >= OUTPUT_DEBUG:
diff --git a/katrain/core/constants.py b/katrain/core/constants.py
index 2fc1fb6..40812bd 100644
--- a/katrain/core/constants.py
+++ b/katrain/core/constants.py
@@ -1,6 +1,6 @@
-VERSION = "1.5.0"
+VERSION = "1.6.1"
HOMEPAGE = "https://github.com/sanderland/katrain"
-CONFIG_MIN_VERSION = "1.4.0" # keep config files from this version
+CONFIG_MIN_VERSION = "1.6.0" # keep config files from this version
OUTPUT_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5
@@ -33,10 +33,12 @@ AI_TENUKI = "ai:p:tenuki"
AI_INFLUENCE = "ai:p:influence"
AI_TERRITORY = "ai:p:territory"
AI_RANK = "ai:p:rank"
+AI_SIMPLE_OWNERSHIP = "ai:simple"
+AI_SETTLE_STONES = "ai:settle"
AI_CONFIG_DEFAULT = AI_RANK
-AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_JIGO]
+AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO] # AI_SETTLE_STONES
AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK]
AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK
AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY
@@ -44,6 +46,8 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_DEFAULT,
AI_RANK,
AI_HANDICAP,
+ AI_SIMPLE_OWNERSHIP,
+ # AI_SETTLE_STONES,
AI_SCORELOSS,
AI_POLICY,
AI_WEIGHTED,
@@ -67,6 +71,8 @@ AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some?
AI_INFLUENCE: -7,
AI_TERRITORY: -7,
AI_RANK: float("nan"),
+ AI_SIMPLE_OWNERSHIP: 2,
+ AI_SETTLE_STONES: 2,
}
AI_OPTION_VALUES = {
@@ -85,8 +91,23 @@ AI_OPTION_VALUES = {
"threshold": [2, 2.5, 3, 3.5, 4, 4.5],
"automatic": "bool",
"pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)],
+ "max_points_lost": [x / 10 for x in range(51)],
+ "settled_weight": [x / 4 for x in range(0, 17)],
+ "opponent_fac": [x / 10 for x in range(-20, 11)],
+ "min_visits": range(1, 10),
+ "attach_penalty": [x / 10 for x in range(-10, 51)],
+ "tenuki_penalty": [x / 10 for x in range(-10, 51)],
+}
+AI_KEY_PROPERTIES = {
+ "kyu_rank",
+ "strength",
+ "weaken_fac",
+ "pick_frac",
+ "pick_n",
+ "automatic",
+ "max_points_lost",
+ "min_visits",
}
-AI_KEY_PROPERTIES = {"kyu_rank", "strength", "weaken_fac", "pick_frac", "pick_n", "automatic"}
CALIBRATED_RANK_ELO = [
diff --git a/katrain/core/engine.py b/katrain/core/engine.py
index a975924..914ad34 100644
--- a/katrain/core/engine.py
+++ b/katrain/core/engine.py
@@ -13,6 +13,7 @@ from kivy.utils import platform
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
from katrain.core.game_node import GameNode
from katrain.core.lang import i18n
+from katrain.core.sgf_parser import Move
from katrain.core.utils import find_package_resource
@@ -85,10 +86,10 @@ class KataGoEngine:
def start(self):
try:
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
- startupinfo = None # stop command box popups on windows/pyinstaller
+ startupinfo = None
if hasattr(subprocess, "STARTUPINFO"):
startupinfo = subprocess.STARTUPINFO()
- startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # stop command box popups on win/pyinstaller
self.katago_process = subprocess.Popen(
self.command,
startupinfo=startupinfo,
@@ -107,6 +108,8 @@ class KataGoEngine:
def on_new_game(self):
self.base_priority += 1
+ for query_id in list(self.queries.keys()):
+ self.terminate_query(query_id)
self.queries = {}
def restart(self):
@@ -131,11 +134,14 @@ class KataGoEngine:
raise EngineDiedException(died_msg)
return ok
+ def wait_to_finish(self):
+ while self.queries and self.katago_process and self.katago_process.poll() is None:
+ time.sleep(0.1)
+
def shutdown(self, finish=False):
process = self.katago_process
if finish and process:
- while self.queries and process.poll() is None:
- time.sleep(0.1)
+ self.wait_to_finish()
if process:
self.katago_process = None
process.terminate()
@@ -196,14 +202,18 @@ class KataGoEngine:
elif "warning" in analysis:
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG)
else:
- del self.queries[query_id]
+ partial_result = analysis.get("isDuringSearch", False)
+ if not partial_result:
+ del self.queries[query_id]
time_taken = time.time() - start_time
self.katrain.log(
- f"[{time_taken:.1f}][{query_id}] KataGo Analysis Received: {analysis.keys()}", OUTPUT_DEBUG,
+ f"[{time_taken:.1f}][{query_id}][{'....' if partial_result else 'done'}] KataGo Analysis Received: {analysis.keys()}",
+ OUTPUT_DEBUG,
)
self.katrain.log(line, OUTPUT_EXTRA_DEBUG)
try:
- callback(analysis)
+ if callback and not analysis.get("noResults", False):
+ callback(analysis, partial_result)
except Exception as e:
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR)
if getattr(self.katrain, "update_state", None): # easier mocking etc
@@ -225,7 +235,10 @@ class KataGoEngine:
self.katago_process.stdin.flush()
except OSError as e:
self.check_alive(os_error=str(e), exception_if_dead=True)
- return # do not raise, since there's nothing to catch it
+
+ def terminate_query(self, query_id):
+ if query_id is not None:
+ self.send_query({"action": "terminate", "terminateId": query_id}, None, None)
def request_analysis(
self,
@@ -236,10 +249,12 @@ class KataGoEngine:
analyze_fast: bool = False,
time_limit=True,
find_alternatives: bool = False,
+ find_local: bool = False,
priority: int = 0,
ownership: Optional[bool] = None,
next_move: Optional[GameNode] = None,
extra_settings: Optional[Dict] = None,
+ report_every: Optional[float] = None,
):
nodes = analysis_node.nodes_from_root
moves = [m for node in nodes for m in node.moves]
@@ -253,6 +268,8 @@ class KataGoEngine:
if analyze_fast and self.config.get("fast_visits"):
visits = self.config["fast_visits"]
+ size_x, size_y = analysis_node.board_size
+
if find_alternatives:
avoid = [
{
@@ -261,10 +278,26 @@ class KataGoEngine:
"untilDepth": 1,
}
]
+ elif find_local:
+ distance = 5
+ last_move = analysis_node.move
+ if last_move is None or last_move.is_pass:
+ return
+ avoid = [
+ {
+ "moves": [
+ Move((x, y)).gtp()
+ for x in range(0, size_x)
+ for y in range(0, size_y)
+ if max(abs(x - last_move.coords[0]), abs(y - last_move.coords[1])) > distance
+ ],
+ "player": analysis_node.next_player,
+ "untilDepth": 1,
+ }
+ ]
else:
avoid = []
- size_x, size_y = analysis_node.board_size
settings = copy.copy(self.override_settings)
if time_limit:
settings["maxTime"] = self.config["max_time"]
@@ -277,15 +310,19 @@ class KataGoEngine:
"analyzeTurns": [len(moves)],
"maxVisits": visits,
"komi": analysis_node.komi,
- "avoidMoves": avoid,
"boardXSize": size_x,
"boardYSize": size_y,
"includeOwnership": ownership and not next_move,
+ "includeMovesOwnership": ownership and not next_move,
"includePolicy": not next_move,
"initialStones": [[m.player, m.gtp()] for m in initial_stones],
"initialPlayer": analysis_node.root.next_player,
"moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": {**settings, **(extra_settings or {})},
}
+ if report_every is not None:
+ query["reportDuringSearchEvery"] = report_every
+ if avoid:
+ query["avoidMoves"] = avoid
self.send_query(query, callback, error_callback, next_move)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits)
diff --git a/katrain/core/game.py b/katrain/core/game.py
index ee4077d..71f72f5 100644
--- a/katrain/core/game.py
+++ b/katrain/core/game.py
@@ -79,12 +79,14 @@ class Game:
self.root.set_property("RU", katrain.config("game/rules"))
self.set_current_node(self.root)
+ self.main_time_used = 0
threading.Thread(
target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True
).start() # return faster, but bypass Kivy Clock
def analyze_all_nodes(self, priority=0, analyze_fast=False):
for node in self.root.nodes_in_tree:
+ node.clear_analysis()
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
# -- move tree functions --
@@ -183,12 +185,22 @@ class Game:
cn = cn.parent
self.set_current_node(cn)
- def redo(self, n_times=1):
+ def redo(self, n_times=1, stop_on_mistake=None):
cn = self.current_node # avoid race conditions
- for _ in range(n_times):
+ for move in range(n_times):
if cn.children:
cn = cn.ordered_children[0]
- self.set_current_node(cn)
+ if (
+ move > 0
+ and stop_on_mistake is not None
+ and cn.points_lost is not None
+ and cn.points_lost >= stop_on_mistake
+ and self.katrain.players_info[cn.player].player_type != PLAYER_AI
+ ):
+ self.set_current_node(cn.parent)
+ return
+ if stop_on_mistake is None:
+ self.set_current_node(cn)
def cycle_children(self, direction):
cn = self.current_node # avoid race conditions
@@ -327,6 +339,7 @@ class Game:
)
else:
visits = cn.analysis_visits_requested + engine.config["max_visits"]
+ self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False)
return
@@ -338,13 +351,13 @@ class Game:
min_visits = min(node.analysis_visits_requested for node in nodes)
visits = min_visits + engine.config["max_visits"]
for node in nodes:
- node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False)
+ node.analyze(engine, visits=visits, priority=-1_000_000, time_limit=False, report_every=None)
self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
return
elif mode == "sweep":
board_size_x, board_size_y = self.board_size
- if cn.analysis_ready:
+ if cn.analysis_exists:
policy_grid = (
var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y))
if self.current_node.policy
@@ -369,32 +382,35 @@ class Game:
visits = engine.config["fast_visits"]
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000_000_000
- elif mode in ["equalize", "alternative"]:
- if not cn.analysis_ready:
+ elif mode == "local": # also do a quick update on current candidates so it doesn't look too weird
+ self.katrain.controls.set_status(i18n._("local analysis"), STATUS_ANALYSIS)
+ cn.analyze(engine, priority=-500, time_limit=False, find_local="local")
+ return
+ elif mode in ["equalize", "alternative", "local"]:
+ if not cn.analysis_complete and mode != "local":
self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node)
return
-
- analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
- priority = -1_000
if mode == "alternative": # also do a quick update on current candidates so it doesn't look too weird
self.katrain.controls.set_status(i18n._("alternative analysis"), STATUS_ANALYSIS)
- cn.analyze(engine, priority=-500, time_limit=False, find_alternatives=True)
+ cn.analyze(engine, priority=-500, time_limit=False, find_alternatives="alternative")
visits = engine.config["fast_visits"]
- else:
+ else: # equalize
visits = max(d["visits"] for d in cn.analysis["moves"].values())
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS)
+ priority = -1_000
+ analyze_moves = [Move.from_gtp(gtp, player=cn.next_player) for gtp, _ in cn.analysis["moves"].items()]
else:
raise ValueError("Invalid analysis mode")
for move in analyze_moves:
if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits:
cn.analyze(
- engine, priority, visits=visits, refine_move=move, time_limit=False
+ engine, priority=priority, visits=visits, refine_move=move, time_limit=False, report_every=None
) # explicitly requested so take as long as you need
def analyze_undo(self, node):
train_config = self.katrain.config("trainer")
move = node.move
- if node != self.current_node or node.auto_undo is not None or not node.analysis_ready or not move:
+ if node != self.current_node or node.auto_undo is not None or not node.analysis_complete or not move:
return
points_lost = node.points_lost
thresholds = train_config["eval_thresholds"]
diff --git a/katrain/core/game_node.py b/katrain/core/game_node.py
index 0a73a3b..de7bc60 100644
--- a/katrain/core/game_node.py
+++ b/katrain/core/game_node.py
@@ -13,7 +13,6 @@ class GameNode(SGFNode):
def __init__(self, parent=None, properties=None, move=None):
super().__init__(parent=parent, properties=properties, move=move)
- self.analysis = {"moves": {}, "root": None}
self.ownership = None
self.policy = None
self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move)
@@ -21,9 +20,13 @@ class GameNode(SGFNode):
self.note = ""
self.move_number = 0
self.time_used = 0
- self.analysis_visits_requested = 0
self.undo_threshold = random.random() # for fractional undos
self.end_state = None
+ self.clear_analysis()
+
+ def clear_analysis(self):
+ self.analysis_visits_requested = 0
+ self.analysis = {"moves": {}, "root": None, "completed": False}
def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None):
properties = copy.copy(super().sgf_properties())
@@ -34,8 +37,8 @@ class GameNode(SGFNode):
show_class = False
if (
self.parent
- and self.parent.analysis_ready
- and self.analysis_ready
+ and self.parent.analysis_exists
+ and self.analysis_exists
and (note or ((save_comments_player or {}).get(self.player, False) and show_class))
):
candidate_moves = self.parent.candidate_moves
@@ -77,16 +80,22 @@ class GameNode(SGFNode):
refine_move=None,
analyze_fast=False,
find_alternatives=False,
+ find_local=False,
+ report_every=0.25,
):
engine.request_analysis(
self,
- lambda result: self.set_analysis(result, refine_move, find_alternatives),
+ callback=lambda result, partial_result: self.set_analysis(
+ result, refine_move, find_alternatives or find_local, partial_result
+ ),
priority=priority,
visits=visits,
analyze_fast=analyze_fast,
time_limit=time_limit,
next_move=refine_move,
find_alternatives=find_alternatives,
+ find_local=find_local,
+ report_every=report_every,
)
def update_move_analysis(self, move_analysis, move_gtp):
@@ -102,24 +111,26 @@ class GameNode(SGFNode):
if cur["visits"] < move_analysis["visits"]:
cur.update(move_analysis)
- def set_analysis(self, analysis_json: Dict, refine_move: Optional[Move], alternatives_mode: bool):
+ def set_analysis(
+ self, analysis_json: Dict, refine_move: Optional[Move], additional_moves: bool, partial_result: bool = False
+ ):
if refine_move:
pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
self.update_move_analysis(
{"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp()
)
else:
- if alternatives_mode:
+ if additional_moves:
for m in analysis_json["moveInfos"]:
- m["order"] += 10 # offset for not making this top
- if refine_move is None and not alternatives_mode:
+ del m["order"] # avoid changing order
+ if refine_move is None and not additional_moves:
for move_dict in self.analysis["moves"].values():
move_dict["order"] = 999 # old moves to end
for move_analysis in analysis_json["moveInfos"]:
self.update_move_analysis(move_analysis, move_analysis["move"])
self.ownership = analysis_json.get("ownership")
self.policy = analysis_json.get("policy")
- if not alternatives_mode:
+ if not additional_moves:
self.analysis["root"] = analysis_json["rootInfo"]
if self.parent and self.move:
analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + (
@@ -128,14 +139,20 @@ class GameNode(SGFNode):
self.parent.update_move_analysis(
analysis_json["rootInfo"], self.move.gtp()
) # update analysis in parent for consistency
+ is_normal_query = refine_move is None and not additional_moves
+ self.analysis["completed"] = self.analysis["completed"] or (is_normal_query and not partial_result)
@property
- def analysis_ready(self):
+ def analysis_exists(self):
return self.analysis["root"] is not None
+ @property
+ def analysis_complete(self):
+ return self.analysis["completed"] and self.analysis["root"] is not None
+
@property
def score(self) -> Optional[float]:
- if self.analysis_ready:
+ if self.analysis_exists:
return self.analysis["root"].get("scoreLead")
def format_score(self, score=None):
@@ -145,7 +162,7 @@ class GameNode(SGFNode):
@property
def winrate(self) -> Optional[float]:
- if self.analysis_ready:
+ if self.analysis_exists:
return self.analysis["root"].get("winrate")
def format_winrate(self, win_rate=None):
@@ -176,12 +193,12 @@ class GameNode(SGFNode):
return ""
text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n"
- if self.analysis_ready:
+ if self.analysis_exists:
score = self.score
if sgf:
text += i18n._("Info:score").format(score=self.format_score(score)) + "\n"
text += i18n._("Info:winrate").format(winrate=self.format_winrate()) + "\n"
- if self.parent and self.parent.analysis_ready:
+ if self.parent and self.parent.analysis_exists:
previous_top_move = self.parent.candidate_moves[0]
if sgf or details:
if previous_top_move["move"] != single_move.gtp():
@@ -202,20 +219,21 @@ class GameNode(SGFNode):
if currmove_pol_rank is not None:
policy_rank_msg = i18n._("Info:policy rank")
text += policy_rank_msg.format(rank=currmove_pol_rank, probability=currmove_pol_prob) + "\n"
- if currmove_pol_rank is None or currmove_pol_rank != 1 and (sgf or details):
+ if currmove_pol_rank != 1 and policy_ranking and (sgf or details):
policy_best_msg = i18n._("Info:policy best")
pol_move, pol_prob = policy_ranking[0][1].gtp(), policy_ranking[0][0]
text += policy_best_msg.format(move=pol_move, probability=pol_prob) + "\n"
if self.auto_undo and sgf:
text += i18n._("Info:teaching undo") + "\n"
- top_pv = self.analysis_ready and self.candidate_moves[0].get("pv")
+ top_pv = self.analysis_exists and self.candidate_moves[0].get("pv")
if top_pv:
text += i18n._("Info:undo predicted PV").format(pv=f"{self.next_player}{' '.join(top_pv)}") + "\n"
- if self.ai_thoughts and (sgf or details):
- text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts)
else:
text = i18n._("No analysis available") if sgf else i18n._("Analyzing move...")
+ if self.ai_thoughts and (sgf or details):
+ text += "\n" + i18n._("Info:AI thoughts").format(thoughts=self.ai_thoughts)
+
if "C" in self.properties:
text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"])
@@ -224,7 +242,7 @@ class GameNode(SGFNode):
@property
def points_lost(self) -> Optional[float]:
single_move = self.move
- if single_move and self.parent and self.analysis_ready and self.parent.analysis_ready:
+ if single_move and self.parent and self.analysis_exists and self.parent.analysis_exists:
parent_score = self.parent.score
score = self.score
return self.player_sign(single_move.player) * (parent_score - score)
@@ -236,8 +254,8 @@ class GameNode(SGFNode):
single_move
and self.parent
and self.parent.parent
- and self.analysis_ready
- and self.parent.parent.analysis_ready
+ and self.analysis_exists
+ and self.parent.parent.analysis_exists
):
parent_parent_score = self.parent.parent.score
score = self.score
@@ -249,7 +267,7 @@ class GameNode(SGFNode):
@property
def candidate_moves(self) -> List[Dict]:
- if not self.analysis_ready:
+ if not self.analysis_exists:
return []
if not self.analysis["moves"]:
polmoves = self.policy_ranking
diff --git a/katrain/gui.kv b/katrain/gui.kv
index 07219f9..9e876d5 100644
--- a/katrain/gui.kv
+++ b/katrain/gui.kv
@@ -32,7 +32,7 @@
#:set CP_SPACING dp(6)
#:set CP_SMALL_SPACING dp(3)
#:set CP_PADDING dp(6)
-#:set NOTES_FONT_SIZE dp(18)
+#:set NOTES_FONT_SIZE sp(18)
#:set EPSILON 1e-9
@@ -126,7 +126,7 @@
pos: [root.pos[i] + (root.size[i] - root.icon_size[i])/2 for i in [0,1]]
source: root.icon
mipmap: True
- color: [1,1,1,0.4] if root.disabled else [1,1,1,1]
+ color: [1,1,1,0.4] if root.disabled else root.color
:
@@ -171,8 +171,6 @@
size: image.size
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text))
-
-
# UI components
:
@@ -187,7 +185,6 @@
:
katrain: app.gui
- branch: branch
circles: [black_circle,white_circle]
mid_circles_container: mid_circles_container
pass_btn: pass_btn
@@ -209,7 +206,7 @@
on_press: root.katrain('undo',999)
NavIconButton:
icon: 'img/Previous-5.png'
- on_press: root.katrain('undo',5)
+ on_press: root.katrain('undo',10)
NavIconButton:
icon: 'img/Previous.png'
on_press: root.katrain('undo')
@@ -233,20 +230,19 @@
on_press: root.katrain('redo')
NavIconButton:
icon: 'img/Next-5.png'
- on_press: root.katrain('redo',5)
+ on_press: root.katrain('redo',10)
NavIconButton:
icon: 'img/Next-End.png'
on_press: root.katrain('redo',999)
NavIconButton:
- id: branch
- icon: 'img/Branch.png'
- on_press: root.katrain('cycle-children',1)
- disabled: True
+ icon: 'img/Next-Mistake.png'
+ color: [0.65,0.05,0.05,1]
+ on_press: root.katrain('next-mistake')
ClickableLabel:
pos_hint: {'center_x': 0.85,'center_y':0.5}
size_hint: None, 1
width: self.height
- canvas:
+ canvas.before:
Color:
rgba: YELLOW
Ellipse:
@@ -264,6 +260,9 @@
Line:
circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, self.width/6
width: 1
+ text: str(root.queries_remaining) if root.queries_remaining >= 3 else ''
+ color: BLACK
+ font_size: 0.5 * self.width / max(2,len(self.text))
on_press: app.gui.controls.set_status(i18n._('ai-ponder'),STATUS_INFO)
@@ -357,14 +356,14 @@
spacing: CP_SPACING
padding: CP_PADDING * 4, CP_PADDING * 2
paused: True
- timeout: self.state[1] <= 0
+ timeout: root.state[1] is not None and root.state[1] <= 0
MDBoxLayout:
size_hint: 0.7,1
MDFloatLayout:
size_hint: 0.4,1
TimerLabel:
color: BACKGROUND_COLOR
- text: '8:88'
+ text: '{}:88'.format('188' if root.state[0] >= 6000 else '88')
font_size: 0.85*self.height
pos_hint: {'right':1,'center_y':0.5}
halign: 'right'
@@ -372,27 +371,27 @@
TimerLabel:
id: timer
timeout: root.timeout
- text: "{:01d}:{:02d}".format(int(root.state[0]+0.99)//60,int((root.state[0]+0.99) % 60) )
+ text: "{:2d}:{:02d}".format(int(root.state[0]+0.99)//60,int((root.state[0]+0.99) % 60) )
font_size: 0.85*self.height
pos_hint: {'right':1,'center_y':0.5}
halign: 'right'
valign: 'middle'
TimerLabel:
size_hint: 0.05,1
- text: "x"
+ text: "x" if root.state[1] is not None else ""
font_size: self.height * 0.3
text_size: self.width, self.height * 0.95
halign: 'right'
valign: 'top'
timeout: root.timeout
TimerLabel:
- size_hint: 0.1,1
+ size_hint: 0.075,1
font_size: self.height * 0.35
text_size: self.size
halign: 'left'
valign: 'top'
id: periods
- text: str(root.state[1])
+ text: str(root.state[1]) if root.state[1] is not None else ""
max_lines: 1
timeout: root.timeout
MDBoxLayout:
@@ -507,8 +506,6 @@
katrain: app.gui
orientation: 'vertical'
players: {'B':B_player,'W':W_player}
- engine_status_col: [1,1,1,1]
- engine_status_pondering: -1
timer:timer
graph: graph
stats: stats
@@ -685,6 +682,7 @@
color: LIGHTGREY
+
:
adaptive_width: True
spacing: CP_SPACING
@@ -700,15 +698,18 @@
color: checkbox.disabled_color if root.disabled else (checkbox.selected_color if checkbox.active else checkbox.unselected_color)
size_hint_x: None
width: self.texture_size[0] + 4
- on_left_press: checkbox.active = not checkbox.active
- MDCheckbox:
+ on_left_press: checkbox.trigger_action()
+ TriStateMDCheckbox:
id: checkbox
disabled: root.disabled
+ tri_state: root.tri_state
size_hint: None,0.5
pos_hint: {'center_y': 0.5}
width: self.height
active: root.default_active
on_active: if app.gui: app.gui.update_state()
+ on_slashed: if app.gui: app.gui.update_state()
+
:
on_close: app.gui.analysis_controls.close_dropdown()
@@ -749,6 +750,11 @@
icon: 'img/Alternative.png'
shortcut: 'f'
on_action: root.katrain("analyze-extra", "alternative")
+ AnalysisMenuItem:
+ text: i18n._("analysis:local")
+ icon: 'img/local.png'
+ shortcut: 'g'
+ on_action: root.katrain("analyze-extra", "local")
AnalysisMenuItem:
text: i18n._("analysis:game")
icon: 'img/analysis.png'
@@ -792,6 +798,7 @@
AnalysisToggle:
id: eval
text: i18n._("analysis:dots")
+ tri_state: True
AnalysisToggle:
id: hints
text: i18n._("analysis:topmoves")
@@ -853,11 +860,12 @@
player_subtype_ai: player_subtype_ai
MDBoxLayout:
size_hint: 0.25, 1
- CircleWithText:
+ ClickableCircle:
size_hint: 0.8, 0.8
pos_hint: {'center_x':0.5,'center_y':0.5}
text: root.player[0]
player: root.player
+ on_left_press: root.parent.swap_players()
MDBoxLayout:
size_hint: 0.5, 1
orientation: 'vertical'
@@ -1059,14 +1067,14 @@
AnalysisControls:
id: analysis_controls
size_hint_y: None
- height: 1 if root.zen > 0 else max(dp(50),min(dp(75),self.width / 13.5))
+ height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5))
opacity: 0 if root.zen > 0 else 1
BadukPanWidget:
id: board_gui
BadukPanControls:
id: board_controls
size_hint_y: None
- height: 1 if root.zen > 0 else max(dp(50),min(dp(75),self.width / 13.5))
+ height: 1 if root.zen > 0 else max(sp(50),min(sp(75),self.width / 13.5))
opacity: 0 if root.zen > 0 else 1
MDBoxLayout:
size_hint_x: None
diff --git a/katrain/gui/badukpan.py b/katrain/gui/badukpan.py
index 611e706..0c921f2 100644
--- a/katrain/gui/badukpan.py
+++ b/katrain/gui/badukpan.py
@@ -42,7 +42,7 @@ class BadukPanWidget(Widget):
Window.bind(mouse_pos=self.on_mouse_pos)
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents)
self.redraw_trigger = Clock.create_trigger(self.redraw)
- self.bind(size=self.redraw_trigger)
+ self.bind(size=self.redraw_trigger, pos=self.redraw_trigger)
Clock.schedule_interval(self.animate_pv, 0.1)
# stone placement functions
@@ -106,8 +106,23 @@ class BadukPanWidget(Widget):
return
katrain = self.katrain
if self.ghost_stone and ("button" not in touch.profile or touch.button == "left"):
- katrain("play", self.ghost_stone)
- self.play_stone_sound()
+ game = self.katrain and self.katrain.game
+ current_node = game and self.katrain.game.current_node
+ if (
+ current_node
+ and not current_node.children
+ and not self.katrain.next_player_info.ai
+ and not self.katrain.controls.timer.paused
+ and self.katrain.play_analyze_mode == MODE_PLAY
+ and self.katrain.config("timer/main_time", 0) * 60 - game.main_time_used <= 0
+ and current_node.time_used < self.katrain.config("timer/minimal_use", 0)
+ ):
+ self.katrain.controls.set_status(
+ i18n._("move too fast").format(num=self.katrain.config("timer/minimal_use", 0)), STATUS_TEACHING
+ )
+ else:
+ katrain("play", self.ghost_stone)
+ self.play_stone_sound()
elif not self.ghost_stone:
xd, xp = self._find_closest(touch.x, self.gridpos_x)
yd, yp = self._find_closest(touch.y, self.gridpos_y)
@@ -125,7 +140,7 @@ class BadukPanWidget(Widget):
katrain.log(f"\nRoot Stats:\n{nodes_here[-1].analysis['root']}", OUTPUT_DEBUG)
katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
katrain.controls.active_comment_node = nodes_here[-1]
- if nodes_here[-1].parent.analysis_ready:
+ if nodes_here[-1].parent.analysis_exists:
self.set_animating_pv(nodes_here[-1].parent.candidate_moves[0]["pv"], nodes_here[-1].parent)
self.ghost_stone = None
@@ -257,6 +272,7 @@ class BadukPanWidget(Widget):
current_node = katrain.game.current_node
game_ended = katrain.game.end_result
full_eval_on = katrain.analysis_controls.eval.active
+ all_dots_off = katrain.analysis_controls.eval.checkbox.slashed
has_stone = {}
drawn_stone = {}
for m in katrain.game.stones:
@@ -280,7 +296,9 @@ class BadukPanWidget(Widget):
placements = node.placements
for m in node.moves + placements:
if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for
- move_eval_on = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on)
+ move_eval_on = (
+ not all_dots_off and show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on)
+ )
if move_eval_on and points_lost is not None:
evalcol = self.eval_color(points_lost, show_dots_for_class)
else:
@@ -454,7 +472,7 @@ class BadukPanWidget(Widget):
source="img/topmove.png",
)
if self.trainer_config["text_point_loss"] and text_on:
- if move_dict["pointsLost"] < 0.05:
+ if -0.05 < move_dict["pointsLost"] < 0.05:
ptloss_text = "0.0"
else:
ptloss_text = f"{-move_dict['pointsLost']:+.1f}"
@@ -484,7 +502,7 @@ class BadukPanWidget(Widget):
for child_node in current_node.children:
move = child_node.move
if move and move.coords is not None:
- if child_node.analysis_ready:
+ if child_node.analysis_exists:
self.active_pv_moves.append(
(move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node)
)
@@ -619,3 +637,4 @@ class AnalysisControls(MDBoxLayout):
class BadukPanControls(MDFloatLayout):
engine_status_col = ListProperty(ENGINE_DOWN_COL)
engine_status_pondering = NumericProperty(-1)
+ queries_remaining = NumericProperty(0)
diff --git a/katrain/gui/controlspanel.py b/katrain/gui/controlspanel.py
index 245c0bd..9d5d45a 100644
--- a/katrain/gui/controlspanel.py
+++ b/katrain/gui/controlspanel.py
@@ -71,6 +71,9 @@ class ControlsPanel(BoxLayout):
self.active_comment_node = None
self.last_timer_update = (None, 0, False)
self.beep = SoundLoader.load("sounds/countdownbeep.wav")
+ self.boing = SoundLoader.load("sounds/boing.wav")
+ if self.boing:
+ self.boing.volume = 0.1
self.beep_start = 5.2
self.timer_interval = 0.07
@@ -121,7 +124,7 @@ class ControlsPanel(BoxLayout):
self.active_comment_node = current_node.children[-1]
elif current_node.parent:
self.active_comment_node = current_node.parent
- elif both_players_are_robots and not current_node.analysis_ready and current_node.parent:
+ elif both_players_are_robots and not current_node.analysis_exists and current_node.parent:
self.active_comment_node = current_node.parent
lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY
@@ -132,7 +135,7 @@ class ControlsPanel(BoxLayout):
teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details
)
- if self.active_comment_node.analysis_ready:
+ if self.active_comment_node.analysis_exists:
self.stats.score = self.active_comment_node.format_score() or ""
self.stats.winrate = self.active_comment_node.format_winrate() or ""
self.stats.points_lost = self.active_comment_node.points_lost
@@ -149,11 +152,13 @@ class ControlsPanel(BoxLayout):
self.info.text = info
def update_timer(self, _dt):
- current_node = self.katrain and self.katrain.game and self.katrain.game.current_node
+ game = self.katrain and self.katrain.game
+ current_node = game and self.katrain.game.current_node
if current_node:
last_update_node, last_update_time, beeping = self.last_timer_update
new_beeping = beeping
now = time.time()
+ main_time = self.katrain.config("timer/main_time", 0) * 60
byo_len = max(1, self.katrain.config("timer/byo_length"))
byo_num = max(1, self.katrain.config("timer/byo_periods"))
sounds_on = self.katrain.config("timer/sound")
@@ -161,9 +166,16 @@ class ControlsPanel(BoxLayout):
ai = player.ai
used_period = False
+ min_use = self.katrain.config("timer/minimal_use", 0)
+ boing_at_remaining = byo_len - min_use
+ main_time_remaining = main_time - game.main_time_used
+
if not self.timer.paused and not ai and self.katrain.play_analyze_mode == MODE_PLAY:
if last_update_node == current_node and not current_node.children:
- current_node.time_used += now - last_update_time
+ if main_time_remaining > 0:
+ game.main_time_used += now - last_update_time
+ else:
+ current_node.time_used += now - last_update_time
else:
current_node.time_used = 0
new_beeping = False
@@ -173,6 +185,7 @@ class ControlsPanel(BoxLayout):
time_remaining += byo_len
player.periods_used += 1
used_period = True
+
if (
self.beep_start - 2 * self.timer_interval < time_remaining < self.beep_start
and player.periods_used < byo_num
@@ -180,6 +193,18 @@ class ControlsPanel(BoxLayout):
new_beeping = True
elif time_remaining > self.beep_start:
new_beeping = False
+
+ if (
+ min_use
+ and not new_beeping
+ and self.boing
+ and boing_at_remaining - self.timer_interval
+ < time_remaining
+ < boing_at_remaining + self.timer_interval
+ and player.periods_used < byo_num
+ ):
+ self.boing.play()
+
else:
new_beeping = False
@@ -198,4 +223,7 @@ class ControlsPanel(BoxLayout):
self.last_timer_update = (current_node, now, new_beeping)
- self.timer.state = (max(0, time_remaining), max(0, periods_rem), ai)
+ if main_time_remaining > 0:
+ self.timer.state = (main_time_remaining, None, ai)
+ else:
+ self.timer.state = (max(0, time_remaining), max(0, periods_rem), ai)
diff --git a/katrain/gui/kivyutils.py b/katrain/gui/kivyutils.py
index a2b953b..703ac55 100644
--- a/katrain/gui/kivyutils.py
+++ b/katrain/gui/kivyutils.py
@@ -23,6 +23,7 @@ from kivymd.uix.behaviors import CircularRippleBehavior, RectangularRippleBehavi
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import BaseFlatButton, BasePressedButton
from kivymd.uix.navigationdrawer import MDNavigationDrawer
+from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, MODE_PLAY, PLAYER_AI
@@ -112,6 +113,7 @@ class AutoSizedRectangleToggleButton(ToggleButtonMixin, AutoSizedRectangleButton
class TransparentIconButton(CircularRippleBehavior, Button):
+ color = ListProperty([1, 1, 1, 1])
icon_size = ListProperty([25, 25])
icon = StringProperty("")
disabled = BooleanProperty(False)
@@ -342,9 +344,12 @@ class PlayerSetupBlock(MDBoxLayout):
def swap_players(self):
player_dump = {bw: p.player_type_dump for bw, p in self.players.items()}
for bw in "BW":
- self.update_players(bw, player_dump["B" if bw == "W" else "W"])
+ self.update_player_params(bw, player_dump["B" if bw == "W" else "W"])
- def update_players(self, bw, player_info): # update sub widget based on gui state change
+ def update_player_params(self, bw, params):
+ self.players[bw].update_widget(**params)
+
+ def update_player_info(self, bw, player_info): # update sub widget based on gui state change
self.players[bw].update_widget(player_type=player_info.player_type, player_subtype=player_info.player_subtype)
@@ -368,14 +373,36 @@ class Timer(BGBoxLayout):
timeout = BooleanProperty(False)
+class TriStateMDCheckbox(MDCheckbox):
+ tri_state = BooleanProperty(False)
+ slashed = BooleanProperty(False)
+
+ def _do_press(self):
+ if not self.tri_state:
+ return super()._do_press()
+ if self.slashed:
+ self.state = "normal"
+ self.slashed = False
+ self.icon = "checkbox-blank-outline"
+ elif self.state == "down":
+ self.state = "normal"
+ self.slashed = True
+ self.icon = "checkbox-blank-off-outline"
+ else:
+ self.state = "down"
+ self.slashed = False
+ self.icon = "checkbox-marked-outline"
+
+
class AnalysisToggle(MDBoxLayout):
text = StringProperty("")
default_active = BooleanProperty(False)
font_name = StringProperty(DEFAULT_FONT)
disabled = BooleanProperty(False)
+ tri_state = BooleanProperty(False)
def trigger_action(self, *args, **kwargs):
- return self.checkbox.trigger_action(*args, **kwargs)
+ return self.checkbox._do_press()
def activate(self, *_args):
self.checkbox.active = True
@@ -559,6 +586,10 @@ class ClickableLabel(LeftButtonBehavior, Label):
pass
+class ClickableCircle(LeftButtonBehavior, CircleWithText):
+ pass
+
+
class ScrollableLabel(ScrollView, BackgroundMixin):
__events__ = ["on_ref_press"]
outline_color = ListProperty([0, 0, 0, 0]) # mixin not working for some reason
diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py
index 43a108c..ec65052 100644
--- a/katrain/gui/popups.py
+++ b/katrain/gui/popups.py
@@ -238,6 +238,7 @@ class ConfigTimerPopup(QuickConfigGui):
p.periods_used = 0
self.katrain.controls.timer.paused = True
self.katrain.game.current_node.time_used = 0
+ self.katrain.game.main_time_used = 0
self.katrain.update_state()
@@ -245,7 +246,7 @@ class NewGamePopup(QuickConfigGui):
def __init__(self, katrain):
super().__init__(katrain)
for bw, info in katrain.players_info.items():
- self.player_setup.update_players(bw, info)
+ self.player_setup.update_player_info(bw, info)
self.rules_spinner.value_refs = [name for abbr, name in katrain.engine.RULESETS_ABBR]
@@ -259,6 +260,20 @@ class NewGamePopup(QuickConfigGui):
self.katrain.update_player(bw, **player_setup.player_type_dump)
self.katrain("new-game")
+ def update_game(self):
+ props = self.collect_properties(self)
+ root = self.katrain.game.root
+ changed = False
+ for k, v in [("RU", props["game/rules"]), ("KM", props["game/komi"])]:
+ current = root.get_property(k)
+ if current != v:
+ changed = True
+ self.katrain.game.root.set_property(k, v)
+ if changed:
+ self.katrain.engine.on_new_game()
+ self.katrain.game.analyze_all_nodes()
+ self.popup.dismiss()
+
def wrap_anchor(widget):
anchor = AnchorLayout()
@@ -341,7 +356,7 @@ class ConfigAIPopup(QuickConfigGui):
self.options_grid.clear_widgets()
self.help_label.text = i18n._(strategy.replace("ai:", "aihelp:"))
for k, v in sorted(mode_settings.items(), key=lambda kv: (kv[0] not in AI_KEY_PROPERTIES, kv[0])):
- self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.25))
+ self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.275))
if k in AI_OPTION_VALUES:
values = AI_OPTION_VALUES[k]
if values == "bool":
@@ -482,20 +497,20 @@ class ConfigPopup(QuickConfigGui):
KATAGOS = {
"win": {
- "OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-windows-x64.zip",
- "Eigen AVX2 (Modern CPUs) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-windows-x64.zip",
- "Eigen (CPU, Non-optimized) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-windows-x64.zip",
+ "OpenCL v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-windows-x64.zip",
+ "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-windows-x64.zip",
+ "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-windows-x64.zip",
"OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-windows-x64.zip",
},
"linux": {
- "OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-linux-x64.zip",
- "Eigen AVX2 (Modern CPUs) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-linux-x64.zip",
- "Eigen (CPU, Non-optimized) v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-linux-x64.zip",
+ "OpenCL v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-opencl-linux-x64.zip",
+ "Eigen AVX2 (Modern CPUs) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-avx2-linux-x64.zip",
+ "Eigen (CPU, Non-optimized) v1.7.0": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-cpu-eigen-linux-x64.zip",
"OpenCL v1.6.1 (bigger boards)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1%2Bbs29/katago-v1.6.1+bs29-gpu-opencl-linux-x64.zip",
},
"just-descriptions": {
- "CUDA v1.6.1 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-windows-x64.zip",
- "CUDA v1.6.1 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-cuda10.2-linux-x64.zip",
+ "CUDA v1.7.0 (Windows)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-windows-x64.zip",
+ "CUDA v1.7.0 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-linux-x64.zip",
},
}
diff --git a/katrain/gui/widgets/movetree.py b/katrain/gui/widgets/movetree.py
index 69e2292..e9f1d73 100644
--- a/katrain/gui/widgets/movetree.py
+++ b/katrain/gui/widgets/movetree.py
@@ -3,7 +3,7 @@ from collections import defaultdict
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Line, Rectangle
from kivy.lang import Builder
-from kivy.properties import ObjectProperty, Clock, NumericProperty, BooleanProperty
+from kivy.properties import BooleanProperty, Clock, NumericProperty, ObjectProperty
from kivy.uix.dropdown import DropDown
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
@@ -11,15 +11,15 @@ from kivymd.app import MDApp
from katrain.gui.kivyutils import BackgroundMixin, draw_circle, draw_text
from katrain.gui.style import (
- WHITE,
- STONE_COLORS,
- LIGHTGREY,
BACKGROUND_COLOR,
- OUTLINE_COLORS,
LIGHTER_BACKGROUND_COLOR,
- YELLOW,
- STONE_TEXT_COLORS,
+ LIGHTGREY,
+ OUTLINE_COLORS,
RED,
+ STONE_COLORS,
+ STONE_TEXT_COLORS,
+ WHITE,
+ YELLOW,
)
@@ -85,6 +85,17 @@ class MoveTreeCanvas(Widget):
self.set_game_node(parent)
self.is_open = False
+ def make_selected_node_main_branch(self):
+
+ if self.menu_selected_node and self.menu_selected_node.parent:
+ node = self.menu_selected_node
+ while node.parent is not None:
+ node.parent.children.remove(node)
+ node.parent.children.insert(0, node)
+ node = node.parent
+ self.set_game_node(self.menu_selected_node)
+ self.is_open = False
+
def switch_branch(self, direction=1):
pos = self.move_pos.get(self.scroll_view_widget.current_node)
if not self.scroll_view_widget:
@@ -96,7 +107,7 @@ class MoveTreeCanvas(Widget):
self.set_game_node(same_x_moves[new_index][1])
def draw_move_tree(self, current_node):
- if not self.scroll_view_widget:
+ if not self.scroll_view_widget or not current_node:
return
spacing = 5
@@ -109,6 +120,7 @@ class MoveTreeCanvas(Widget):
stack = root.ordered_children[::-1]
next_y_pos = defaultdict(int) # x pos -> max y pos
children = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw!
+ children[root] = root.ordered_children
while stack:
move = stack.pop()
x = move.depth
@@ -176,6 +188,9 @@ class MoveTree(ScrollView, BackgroundMixin):
def delete_selected_node(self):
self.move_tree_canvas.delete_selected_node()
+ def make_selected_node_main_branch(self):
+ self.move_tree_canvas.make_selected_node_main_branch()
+
def scroll_to_pixel(self, x, y):
if not self._viewport:
return
@@ -219,5 +234,11 @@ Builder.load_string(
shortcut: ''
on_action: root.katrain.controls.move_tree.delete_selected_node()
-background_color: LIGHTER_BACKGROUND_COLOR
+ MoveTreeDropdownItem:
+ text: i18n._("Make Main Branch")
+ icon: 'img/Branch.png'
+ shortcut: ''
+ on_action: root.katrain.controls.move_tree.make_selected_node_main_branch()
+ -background_color: LIGHTER_BACKGROUND_COLOR
"""
)
diff --git a/katrain/gui/widgets/selection_slider.py b/katrain/gui/widgets/selection_slider.py
index d15ec14..82bb261 100644
--- a/katrain/gui/widgets/selection_slider.py
+++ b/katrain/gui/widgets/selection_slider.py
@@ -127,7 +127,7 @@ KV = """
Label:
id: label
text: slider.values[slider.index][1]
- font_size: dp(12)
+ font_size: sp(12)
lang_change_tracking: i18n._('') # for font
halign: "center"
color: root.thumb_color if root.active else [0, 0, 0, 0]
diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo
index 7254b38..e98a995 100644
Binary files a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
index 2622d3a..d1a2f98 100644
--- a/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/cn/LC_MESSAGES/katrain.po
@@ -521,7 +521,6 @@ msgstr "读秒声音开启"
msgid "tab:info-details"
msgstr "详细"
-#. TODO: updated
msgid "teaching undo message"
msgstr "撤销第 {move} 手棋因为损失超过 {points_lost:.1f} 目. 悬停在该手棋 上方查看信息. 请再次尝试."
@@ -640,3 +639,54 @@ msgstr "寻找其他走法"
#. status message
msgid "alternative analysis"
msgstr "正在搜索其他走法"
+
+msgid "minimal time use"
+msgstr "最少思考时间 (秒)"
+
+msgid "move too fast"
+msgstr "请思考 {num} 秒再下棋"
+
+msgid "main time"
+msgstr "保留时间 (分钟)"
+
+msgid "aihelp:simple"
+msgstr ""
+"简化局面的风格。 "
+"所有损失最高'max_points_lost'目和至少'min_visits'计算深度的着法当中,偏向于损失较小,乙方空定型(偏向值settled_weight),对方空定型(偏向值settled_weight"
+" * opponent "
+"fac),避免脱先(5路以上,偏向值tenuki_penalty),避免碰(偏向值attachment_penalty)的手法。偏向值可以设为复数捣乱局面,宽根噪声(通用设置)可以棋力代价更加符合风格。"
+" "
+
+msgid "ai:simple"
+msgstr "简明风格"
+
+msgid "analysis:local"
+msgstr "寻找局部走法"
+
+msgid "local analysis"
+msgstr "正在寻找局部走法"
+
+msgid "change current game"
+msgstr ""
+"更新贴目\n"
+"规则在当前对局"
+
+msgid "Make Main Branch"
+msgstr "设为实战变化"
+
+#. TODO - DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Plays moves that lead to clarity on stones' status, favouring solid moves "
+"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
+"points and with at least 'min_visits', prefer moves that lose fewer points, "
+"settle own stones more (with importance settled_weight), settle opponent's "
+"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
+"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
+" attachment_penalty). Weights can be negative to make it complicate the game"
+" instead, and wide root noise (in general settings) can be used to further "
+"play according to the style at the cost of strength. "
+
+#. TODO - DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Style"
diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo
index 5c942fc..0d41741 100644
Binary files a/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/de/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
index 722e7ce..376b311 100644
--- a/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/de/LC_MESSAGES/katrain.po
@@ -671,12 +671,12 @@ msgstr ""
msgid "show point loss as text"
msgstr "Zeig den Punkteverlust auf Beste Züge"
-#. TODO - updated
msgid "teaching undo message"
msgstr ""
-"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bewege "
-"Mauszeiger über den Zug,um die erwartete Widerlegung zu sehen. Bitte "
-"versuche es erneut."
+"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bitte "
+"versuche es erneut. Bewege den Mauszeiger über den Zug, um die erwartete "
+"Widerlegung zu sehen, oder setze ein Häkchen bei “Erwartetes Gebiet” (t), um"
+" die Bereiche des Bretts zu sehen, wo Punktverlust zu erwarten ist."
#. resign the game
msgid "resign"
@@ -689,3 +689,49 @@ msgstr "Finde alternative Züge"
#. - status message
msgid "alternative analysis"
msgstr "Suche nach alternativen Zügen"
+
+#. TODO - ignore moves if timer is on and less than this is used
+msgid "minimal time use"
+msgstr "Minimal time use in byo-yomi (s)"
+
+msgid "move too fast"
+msgstr "Denke mindestens {num} Sekunden nach, bevor Du setzt."
+
+msgid "main time"
+msgstr "Hauptbedenkzeit (Min.)"
+
+msgid "aihelp:simple"
+msgstr ""
+"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von "
+"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens "
+"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren und"
+"eigenes Gebiet mehr sichern."
+
+msgid "ai:simple"
+msgstr "Einfacher Stil"
+
+#. TODO - DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Spielt Züge, die dazu zu einer Vereinfachung des Brettzustandes führen. Von "
+"Zügen,die höchstens 'max_points_lost' Punkte verlieren und mit wenigstens "
+"'min_visits' Visits, bevorzuge die Züge, die weniger Punkte verlieren, "
+"eigenes Gebiet mehr sichern"
+
+#. TODO - DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Style"
+
+msgid "analysis:local"
+msgstr "Finde lokale Antworten"
+
+msgid "local analysis"
+msgstr "Suche nach lokalen Antworten"
+
+msgid "change current game"
+msgstr ""
+"Aktualisiere Komi/Regeln\n"
+" im aktuellen Spiel"
+
+msgid "Make Main Branch"
+msgstr "Zum Hauptzweig machen"
diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo
index 9c7f8dc..246353d 100644
Binary files a/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/en/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
index c5b79e3..72393d1 100644
--- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po
@@ -106,6 +106,10 @@ msgstr "Deeper full game analysis"
msgid "analysis:alternatives"
msgstr "Find alternative moves"
+#. Analyze again but allow only nearby moves
+msgid "analysis:local"
+msgstr "Find local responses"
+
#. Keep improving current position analysis when there is nothing else to do
msgid "analysis:continuous"
msgstr "Toggle continuous analysis"
@@ -125,6 +129,10 @@ msgstr "Analyzing entire board using {visits} visits/move"
msgid "alternative analysis"
msgstr "Searching for alternative moves"
+#. status message
+msgid "local analysis"
+msgstr "Searching for local responses"
+
#. status message
msgid "equalizing analysis"
msgstr "Equalizing analysis of candidate moves to {visits} visits"
@@ -293,14 +301,25 @@ msgid "timer settings"
msgstr "Adjust Timer Settings"
msgid "byoyomi length"
-msgstr "Time per Byo-yomi Period"
+msgstr "Time per byo-yomi period (s)"
msgid "byoyomi periods"
-msgstr "Number of Periods"
+msgstr "Number of periods"
msgid "count down sound"
msgstr "Sound on"
+#. in minutes
+msgid "main time"
+msgstr "Main time (min.)"
+
+#. ignore moves if timer is on and less than this is used
+msgid "minimal time use"
+msgstr "Minimal time use in byo-yomi (s)"
+
+msgid "move too fast"
+msgstr "Think for at least {num} seconds before playing."
+
#. submit button
msgid "update timer"
msgstr "Update Timer"
@@ -351,6 +370,12 @@ msgstr "Use x:y (e.g. 19:9) for non-square size"
msgid "new game"
msgstr "Start New Game"
+#. change komi etc in current game
+msgid "change current game"
+msgstr ""
+"Update komi/rules\n"
+"in active game"
+
# teacher settings from here
msgid "teacher settings"
msgstr "Edit Teacher Settings"
@@ -560,6 +585,38 @@ msgstr ""
"perspective. When `automatic` is set, KaTrain will find a suitable value "
"automatically."
+msgid "ai:simple"
+msgstr "Simple Style"
+
+msgid "aihelp:simple"
+msgstr ""
+"Plays moves that lead to simplifying the board state. Of moves that lose at "
+"most 'max_points_lost' points and with at least 'min_visits', prefer moves "
+"that lose fewer points, settle own territory more (with importance "
+"settled_weight), settle opponent's territory more (importance settled_weight"
+" * opponent_fac), avoid tenuki (more than 5 spaces, importance "
+"tenuki_penalty), and avoid attachments (importance attachment_penalty). "
+"Weights can be negative to make it complicate the game instead, and wide "
+"root noise (in general settings) can be used to increase play according to "
+"the style at the cost of strength. "
+
+#. DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Style"
+
+#. DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Plays moves that lead to clarity on stones' status, favouring solid moves "
+"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
+"points and with at least 'min_visits', prefer moves that lose fewer points, "
+"settle own stones more (with importance settled_weight), settle opponent's "
+"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
+"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
+" attachment_penalty). Weights can be negative to make it complicate the game"
+" instead, and wide root noise (in general settings) can be used to further "
+"play according to the style at the cost of strength. "
+
msgid "ai:jigo"
msgstr "KataJigo"
@@ -691,6 +748,10 @@ msgstr "Number of visits per move"
msgid "Delete Node"
msgstr "Delete Node"
+#. on move tree editing
+msgid "Make Main Branch"
+msgstr "Make Main Branch"
+
#. theme label
msgid "theme"
msgstr "Colour Theme"
diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo
index 75031f1..e1efb06 100644
Binary files a/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/es/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
index 1124967..63d6036 100644
--- a/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/es/LC_MESSAGES/katrain.po
@@ -697,3 +697,58 @@ msgstr "Find alternative moves"
#. TODO - status message
msgid "alternative analysis"
msgstr "Searching for alternative moves"
+
+#. TODO - ignore moves if timer is on and less than this is used
+msgid "minimal time use"
+msgstr "Minimal time use in byo-yomi (s)"
+
+#. TODO
+msgid "move too fast"
+msgstr "Think for at least {num} seconds before playing."
+
+#. TODO - in minutes
+msgid "main time"
+msgstr "Main time (min.)"
+
+#. TODO
+msgid "aihelp:simple"
+msgstr "Plays moves that lead to simplifying the board state."
+
+#. TODO
+msgid "ai:simple"
+msgstr "Simple Style"
+
+#. TODO - DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Plays moves that lead to clarity on stones' status, favouring solid moves "
+"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
+"points and with at least 'min_visits', prefer moves that lose fewer points, "
+"settle own stones more (with importance settled_weight), settle opponent's "
+"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
+"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
+" attachment_penalty). Weights can be negative to make it complicate the game"
+" instead, and wide root noise (in general settings) can be used to further "
+"play according to the style at the cost of strength. "
+
+#. TODO - DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Style"
+
+#. TODO - Analyze again but allow only nearby moves
+msgid "analysis:local"
+msgstr "Find local responses"
+
+#. TODO - status message
+msgid "local analysis"
+msgstr "Searching for local responses"
+
+#. TODO - change komi etc in current game
+msgid "change current game"
+msgstr ""
+"Update komi/rules\n"
+"in active game"
+
+#. TODO - on move tree editing
+msgid "Make Main Branch"
+msgstr "Make Main Branch"
diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo
index 83b7aa1..187adca 100644
Binary files a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
index bb070f0..e039b95 100644
--- a/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/fr/LC_MESSAGES/katrain.po
@@ -93,7 +93,7 @@ msgid "analysis:extra"
msgstr "Analyser davantage"
msgid "analysis:equalize"
-msgstr "Égaliser l’analyse des coups"
+msgstr "Égaliser l'analyse des coups"
msgid "analysis:sweep"
msgstr "Analyser tous les possibles"
@@ -105,7 +105,7 @@ msgid "analysis:continuous"
msgstr "Lancer/Arrêter l'analyse continue"
msgid "analysis:aimove"
-msgstr "Jouer le coup de l’IA"
+msgstr "Jouer le coup de l'IA"
#. label in little popup for analyzing entire game
msgid "reanalyze max visits"
@@ -525,9 +525,10 @@ msgstr ""
#. TODO: updated
msgid "teaching undo message"
msgstr ""
-"Le coup {move} a été annulé car il perdait {points_lost:.1f} points. Vous "
-"pouvez survoler le coup pour voir la réfutation. Veuillez jouer un nouveau "
-"coup."
+"Le coup {move} a été annulé car il perdait {points_lost:.1f} points.Vous "
+"pouvez consulter les Prévisions territoriales (t) pour identifier les "
+"régions concernées, et survoler le coup pour connaître sa réfutation.Pour "
+"continuer la partie, veuillez jouer un nouveau coup."
msgid "sweep analysis"
msgstr "Analyse de tous les possibles ({visits} visites/coup)."
@@ -721,3 +722,57 @@ msgstr "Proposer des coups différents"
msgid "alternative analysis"
msgstr "Recherche de coups différents."
+
+msgid "minimal time use"
+msgstr ""
+"Temps de réflexion \n"
+"minimum (s/période)"
+
+msgid "move too fast"
+msgstr "Réfléchissez au moins {num} secondes avant de jouer."
+
+msgid "main time"
+msgstr "Temps principal (min)"
+
+msgid "aihelp:simple"
+msgstr ""
+"Par défaut, le moteur de jeu cherche à simplifier la position. \n"
+"Il joue un coup parmi ceux considérés au moins 'min_visits' fois et perdant au plus 'max_points_lost'. \n"
+"'settled_weight' le pousse à consolider ses territoires, et ceux adverses une fois multiplié par 'opponent_fac'. \n"
+"'tenuki_penalty' le pousse à jouer localement ; 'attach_penalty' à éviter les coups de contact direct. \n"
+"Quand elles sont possibles, les valeurs négatives favorisent au contraire un style de jeu compliqué. \n"
+"Élargir le Champ des possibles (cf. Paramètres généraux) peaufine le style au détriment du niveau de jeu."
+
+msgid "ai:simple"
+msgstr "Modéré"
+
+#. TODO - DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Plays moves that lead to clarity on stones' status, favouring solid moves "
+"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
+"points and with at least 'min_visits', prefer moves that lose fewer points, "
+"settle own stones more (with importance settled_weight), settle opponent's "
+"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
+"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
+" attachment_penalty). Weights can be negative to make it complicate the game"
+" instead, and wide root noise (in general settings) can be used to further "
+"play according to the style at the cost of strength. "
+
+#. TODO - DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Stones Style"
+
+msgid "analysis:local"
+msgstr "Proposer des coups locaux"
+
+msgid "local analysis"
+msgstr "Recherche de coups locaux."
+
+msgid "change current game"
+msgstr ""
+"Mettre à jour\n"
+"la partie en cours"
+
+msgid "Make Main Branch"
+msgstr "Mettre en branche principale"
diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo
index 9c28dd2..1667cd0 100644
Binary files a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
index 705244c..bc6154c 100644
--- a/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/jp/LC_MESSAGES/katrain.po
@@ -679,3 +679,55 @@ msgstr ""
#. status message
msgid "alternative analysis"
msgstr "他の候補手を分析中"
+
+#. ignore moves if timer is on and less than this is used
+msgid "minimal time use"
+msgstr "早打ち禁止秒数"
+
+msgid "move too fast"
+msgstr "早打ちすぎです。{num}秒は考えてください。"
+
+#. in minutes
+msgid "main time"
+msgstr "持ち時間(分)"
+
+msgid "aihelp:simple"
+msgstr ""
+"盤面の単純化をめざして, 次の基準で手を選びます (括弧内は重視度): 損する目数が少ない, 自分の確定地が多い (settled_weight), "
+"相手も確定地が多い (settled_weight × opponent fac), 相手が打った手の近辺…縦横とも3間以内 "
+"(tenuki_penalty), 相手の石に接触しない (attachment_penalty). ただし, 損が大きすぎたり "
+"(max_points_lost目以上) 探索手数が少なすぎたり (min_visits手未満) する候補手は除きます. 重視度を負にすれば, "
+"逆に盤面の複雑化をめざします. また, 一般設定のwide root noiseを大きくすれば, 強さは犠牲になりますが, "
+"より前述の基準に忠実な打ち方をします."
+
+msgid "ai:simple"
+msgstr "簡明派"
+
+msgid "aihelp:settle"
+msgstr ""
+"複雑なサバキよりも, 石の生死をはっきりさせる堅実な手を好みます. 具体的には, 次の基準で手を選びます (括弧内は重視度): 損する目数が少ない, "
+"自分の生死が明確 (settled_weight), 相手の生死も明確 (settled_weight × opponent fac), "
+"相手が打った手の近辺…縦横とも3間以内 (tenuki_penalty), 相手の石に接触しない (attachment_penalty). ただし, "
+"損が大きすぎたり (max_points_lost目以上) 探索手数が少なすぎたり (min_visits手未満) する候補手は除きます. "
+"重視度を負にすれば, 逆に盤面の複雑化をめざします. また, 一般設定のwide root noiseを大きくすれば, 強さは犠牲になりますが, "
+"より前述の基準に忠実な打ち方をします."
+
+msgid "ai:settle"
+msgstr "堅実派"
+
+#. Analyze again but allow only nearby moves
+msgid "analysis:local"
+msgstr "近辺の応手に絞って分析"
+
+#. status message
+msgid "local analysis"
+msgstr "近辺の応手を分析中"
+
+msgid "change current game"
+msgstr ""
+"現対局のコミ・\n"
+"ルールを変更"
+
+#. on move tree editing
+msgid "Make Main Branch"
+msgstr "こちらを主分岐に"
diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo
index c14eca3..d2aadef 100644
Binary files a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
index eaf984b..30e1461 100644
--- a/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/ko/LC_MESSAGES/katrain.po
@@ -546,11 +546,6 @@ msgstr ""
"`threshold`선 밑의 수를 더 많이 뽑은 후 그 중 최선의 수를 둡니다. `line_weight`을 증가시키면 바둑판 중앙에 있는"
" 수를 더욱 꺼리게 됩니다. 판이 `endgame` 비율만큼 차면 평범하게 둡니다."
-#. TODO: updated
-msgid "teaching undo message"
-msgstr ""
-"{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 커서를 올려 상대의 대응책을 보십시오. 다시 두어 주세요."
-
msgid "aihelp:p:rank"
msgstr ""
"제한적인 착수 가능 목록에서 무작위로 다음 착수를 선택한 후 그 중 최선의 수를 둡니다. 더 강한 설정은 최선의 수를 더 큰 목록에서 "
@@ -654,3 +649,58 @@ msgstr "다른 수 찾기"
msgid "alternative analysis"
msgstr "다른 수를 찾는 중입니다"
+
+msgid "teaching undo message"
+msgstr ""
+"{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 다시 두어 주세요. 힌트를 보려면, 커서를 올려 상대의 "
+"대응책을 보거나, 어디서 집 손해를 보는지 집 모양을 통해 (t) 알아보십시오. "
+
+msgid "minimal time use"
+msgstr "이보다 빨리 착수한 수 무시"
+
+msgid "move too fast"
+msgstr "최소한 {num}초만큼 생각하고 착수해 주십시오"
+
+msgid "main time"
+msgstr "제한 시간(분)"
+
+msgid "aihelp:simple"
+msgstr ""
+"국면을 단순화하는 수를 둡니다. 최대 'max_point_lost'집 손해를 보고 최소 'min_visits'번 탐색한 수 중에, 집 "
+"손해가 적고, 자신의 집을 굳히며(settled_weight에 따라 조절), 상대의 집도 굳히고(settled_weight * "
+"opponent fac에 따라 조절), 붙임수를 피하는(attachment_penalty에 따라 조절) 수를 둡니다. 가중치가 음수일 "
+"때는 반대로 국면을 복잡하게 만듭니다."
+
+msgid "ai:simple"
+msgstr "간명한 기풍"
+
+msgid "analysis:local"
+msgstr "국지적인 응수 찾기"
+
+msgid "local analysis"
+msgstr "국지적인 응수를 찾고 있습니다"
+
+msgid "change current game"
+msgstr ""
+"이 판에 대해\n"
+"덤과 규칙 등 조절"
+
+#. TODO - DEPRECATED
+msgid "ai:settle"
+msgstr "Settle Style"
+
+#. TODO - DEPRECATED
+msgid "aihelp:settle"
+msgstr ""
+"Plays moves that lead to clarity on stones' status, favouring solid moves "
+"rather than complex sabaki. Of moves that lose at most 'max_points_lost' "
+"points and with at least 'min_visits', prefer moves that lose fewer points, "
+"settle own stones more (with importance settled_weight), settle opponent's "
+"stones more (importance settled_weight * opponent_fac), avoid tenuki (more "
+"than 5 spaces, importance tenuki_penalty), and avoid attachments (importance"
+" attachment_penalty). Weights can be negative to make it complicate the game"
+" instead, and wide root noise (in general settings) can be used to further "
+"play according to the style at the cost of strength. "
+
+msgid "Make Main Branch"
+msgstr "주 변화로 만들기"
diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo
index 5459d12..0340107 100644
Binary files a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo and b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.mo differ
diff --git a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
index bfed209..2f68fa8 100644
--- a/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
+++ b/katrain/i18n/locales/ru/LC_MESSAGES/katrain.po
@@ -407,7 +407,7 @@ msgid "engine:max_time"
msgstr "Максимальное время анализа"
msgid "engine:wide_root_noise"
-msgstr "Высокий уровень шума (увеличивает разнообразие рассматриваемых ходов)"
+msgstr "Уровень шума (увеличивает разнообразие рассматриваемых ходов)"
msgid "engine:wide_root_noise:hint"
msgstr ""
@@ -684,3 +684,57 @@ msgstr "Поискать альтернативные ходы"
msgid "alternative analysis"
msgstr "Поиск альтернативных ходов"
+
+msgid "minimal time use"
+msgstr "Минимально потраченное время в бёёми (сек.)"
+
+msgid "move too fast"
+msgstr "Подумайте как минимум {num} секунд."
+
+msgid "main time"
+msgstr "Основное время (мин.)"
+
+msgid "aihelp:simple"
+msgstr ""
+"Делает ходы, упрощающие позицию на доске. Из ходов, теряющих не более чем "
+"'max_points_lost' очков и имеющих как минимум 'min_visits', предпочитает "
+"ходы, укрепляющие собственную территорию (коэффициент settled_weight), "
+"укрепляющие территорию оппонента (коэффициент settled_weight * "
+"opponent_fac), избегает тэнуки (более 5 пунктов, коэффициент "
+"tenuki_penalty), и избегает прилипаний (коэффициент attachment_penalty). "
+"Коэффициенты могут быть отрицательными, чтобы напротив сделать игру сложнее."
+" 'Уровень шума' (в 'Настройках системы') может быть использован для "
+"увеличения вариативности игры за счёт небольшой потери силы. "
+
+msgid "ai:simple"
+msgstr "Простой стиль"
+
+msgid "aihelp:settle"
+msgstr ""
+"Делает ходы, ведущие к ясности в статусе камней, предпочитая надёжные ходы, "
+"нежели сложное сабаки. Из ходов, теряющих не более чем 'max_points_lost' "
+"очков и имеющих как минимум 'min_visits', предпочитает ходы, укрепляющие "
+"собственную территорию (коэффициент settled_weight), укрепляющие территорию "
+"оппонента (коэффициент settled_weight * opponent_fac), избегает тэнуки "
+"(более 5 пунктов, коэффициент tenuki_penalty), и избегает прилипаний "
+"(коэффициент attachment_penalty). Коэффициенты могут быть отрицательными, "
+"чтобы напротив сделать игру сложнее. 'Уровень шума' (в 'Настройках системы')"
+" может быть использован для увеличения вариативности игры за счёт небольшой "
+"потери силы. "
+
+msgid "ai:settle"
+msgstr "Надёжный стиль"
+
+msgid "analysis:local"
+msgstr "Найти локальные ответы"
+
+msgid "local analysis"
+msgstr "Поиск локальных ответов"
+
+msgid "change current game"
+msgstr ""
+"Изменить коми/правила\n"
+"в текущей игре"
+
+msgid "Make Main Branch"
+msgstr "Сделать главной веткой"
diff --git a/katrain/img/Next-Mistake.png b/katrain/img/Next-Mistake.png
new file mode 100644
index 0000000..a1ccfb7
Binary files /dev/null and b/katrain/img/Next-Mistake.png differ
diff --git a/katrain/img/local.png b/katrain/img/local.png
new file mode 100644
index 0000000..cc4506a
Binary files /dev/null and b/katrain/img/local.png differ
diff --git a/katrain/popups.kv b/katrain/popups.kv
index c9c5265..59fd232 100644
--- a/katrain/popups.kv
+++ b/katrain/popups.kv
@@ -8,8 +8,8 @@
#:set CP_SMALL_SPACING 3
#:set CP_PADDING 6
-#:set INPUT_FONT_SIZE dp(25)
-#:set DESC_FONT_SIZE dp(20)
+#:set INPUT_FONT_SIZE sp(25)
+#:set DESC_FONT_SIZE sp(20)
#:set LIGHTGREY [0.7,0.7,0.7,1]
#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR
@@ -32,7 +32,7 @@
line_color_focus: TEXT_COLOR
:
- font_size: dp(16)
+ font_size: sp(16)
multiline: False
helper_text_mode: "on_error"
helper_text: i18n._("file not found")
@@ -179,13 +179,18 @@
orientation: 'horizontal'
GridLayout:
cols: 2
- rows: 4
+ rows: 5
spacing: CP_SPACING
DescriptionLabel:
text: i18n._("general:sgf_save")
AnchorLayout:
LabelledPathInput:
input_property: "general/sgf_save"
+ DescriptionLabel:
+ text: i18n._('count down sound')
+ AnchorLayout:
+ LabelledCheckBox:
+ input_property: "timer/sound"
DescriptionLabel:
text: i18n._("general:anim_pv_time")
AnchorLayout:
@@ -250,7 +255,7 @@
AutoSizedRoundedRectangleButton:
padding_x: 15
size_hint: None,None
- height: dp(40)
+ height: sp(40)
text: i18n._("update settings")
on_press: root.update_config(True)
@@ -319,9 +324,15 @@
:
MDGridLayout:
cols: 2
- rows: 3
+ rows: 4
spacing: 1.5 * CP_SPACING
size_hint: 1,2.5
+ DescriptionLabel:
+ text: i18n._('main time')
+ AnchorLayout:
+ LabelledIntInput:
+ text: "10"
+ input_property: "timer/main_time"
DescriptionLabel:
text: i18n._('byoyomi length')
AnchorLayout:
@@ -335,10 +346,11 @@
text: "5"
input_property: "timer/byo_periods"
DescriptionLabel:
- text: i18n._('count down sound')
+ text: i18n._('minimal time use')
AnchorLayout:
- LabelledCheckBox:
- input_property: "timer/sound"
+ LabelledIntInput:
+ text: "0"
+ input_property: "timer/minimal_use"
AnchorLayout:
size_hint: 1,1
AutoSizedRoundedRectangleButton:
@@ -443,13 +455,22 @@
input_property: 'game/clear_cache'
SmallDescriptionLabel:
text: i18n._('avoids replaying')
- AnchorLayout:
- size_hint: 1,1
- AutoSizedRoundedRectangleButton:
- padding_x: 15
- size_hint: None,0.5
- text: i18n._("new game")
- on_press: root.update_config(True)
+ BoxLayout:
+ size_hint: 1,0.75
+ AnchorLayout:
+ size_hint: 0.5, 1
+ AutoSizedRoundedRectangleButton:
+ padding_x: 15
+ size_hint: None,0.5
+ _font_size: self.height*0.35
+ text: i18n._("change current game")
+ on_press: root.update_game()
+ AnchorLayout:
+ AutoSizedRoundedRectangleButton:
+ padding_x: 15
+ size_hint: None,0.66
+ text: i18n._("new game")
+ on_press: root.update_config(True)
:
@@ -457,7 +478,7 @@
textbox: textbox
SelectionSlider:
id: slider
- size_hint: 2,1
+ size_hint: 2.25,1
values: root.values
on_change: textbox.text = str(slider.value)
track_color: LIGHTGREY
diff --git a/katrain/sounds/boing.wav b/katrain/sounds/boing.wav
new file mode 100644
index 0000000..2cee3df
Binary files /dev/null and b/katrain/sounds/boing.wav differ
diff --git a/setup.py b/setup.py
index f5f16d2..2e114ee 100644
--- a/setup.py
+++ b/setup.py
@@ -41,16 +41,19 @@ setup(
install_requires=[
"wheel",
"setuptools",
+ "urllib3",
"importlib_resources ;python_version<'3.7'",
- "pygame;platform_system=='Darwin'", # some mac versions need this for kivy
"cython>=0.24,<=0.29.14,!=0.27,!=0.27.2", # kivy wants this
+ "kivy==2.0.0rc2;platform_system=='Darwin'", # rc3 failing on mac
+ "kivy>=2.0.0rc2;platform_system!='Darwin'", # just use the latest on linux?
+ "pygame;platform_system=='Darwin'", # some mac versions need this for kivy
"kivy_deps.glew;platform_system=='Windows'",
"kivy_deps.sdl2;platform_system=='Windows'",
"kivy_deps.gstreamer;platform_system=='Windows'",
- "kivy==2.0.0rc2", # rc3 failing on mac
"kivymd>=0.104.1",
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
],
+ dependency_links=["https://kivy.org/downloads/simple/"],
python_requires=">=3.6, <4",
entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]},
classifiers=[