Merge pull request #225 from sanderland/v1.6.0

V1.6.0
This commit is contained in:
sanderland authored and GitHub committed 2020-11-23 20:05:01 +09:00
commit 02d4e839a8
47 files changed
+981 -192

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
# These are supported funding model platforms # These are supported funding model platforms
github: sanderland # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 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 open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+2
View File
@@ -16,6 +16,8 @@ tmp.pickle
my my
logs logs
callgrind.* callgrind.*
cpp/out
.vs
# debug # debug
outdated_log.txt outdated_log.txt
+2 -1
View File
@@ -52,6 +52,7 @@ Many thanks to these additional authors:
* "fohristiwhirl" for the Gibo and NGF formats parsing code. * "fohristiwhirl" for the Gibo and NGF formats parsing code.
* "kaorahi" for bug fixes and SGF parser improvements. * "kaorahi" for bug fixes and SGF parser improvements.
* "ajkenny84" for the red-green colourblind theme. * "ajkenny84" for the red-green colourblind theme.
* Lukasz Wierzbowski for the ability to paste urls for sgfs and helping fix alt-gr issues.
## Translators ## Translators
@@ -62,7 +63,7 @@ Many thanks to the following contributors for translations.
* German: "nimets123" with contributions from "trohde" * German: "nimets123" with contributions from "trohde"
* Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community * Spanish: Sergio Villegas ("serpiente") with contributions from the Spanish OGS community
* Russian: Dmitry Ivankov and Alexander Kiselev * 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" * Japanese: "kaorahi"
## Additional thanks to ## Additional thanks to
+1 -1
View File
@@ -53,7 +53,7 @@ Simply download and run, everything is included.
### <a name="LinuxQuick"></a>Quick install guide ### <a name="LinuxQuick"></a>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 `pip3 install -U katrain`
* Run the program by executing `katrain` in a terminal. * Run the program by executing `katrain` in a terminal.
+2 -1
View File
@@ -1,10 +1,11 @@
This repository includes: 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 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: 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 - 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 - 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. 3. The True Type Font DIGITAL-7 version 1.02 by Sizenko Alexander, which is free for non-commercial use.
+4
View File
@@ -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 * **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 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). [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 * **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. 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). * **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. * **[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. * **[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. * **[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. * **[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. * **[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 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 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. * **[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 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. * **[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. * **[click on a move]**: See detailed statistics for a previous move, along with expected variation that was best instead of this move.
+1 -1
View File
@@ -26,7 +26,7 @@ for lang in locales:
pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po") pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po")
po[lang] = polib.pofile(pofile[lang]) po[lang] = polib.pofile(pofile[lang])
for entry in po[lang].translated_entries(): 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) todos[lang].append(entry)
strings_to_langs[entry.msgid][lang] = entry strings_to_langs[entry.msgid][lang] = entry
strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr)) strings_to_keys[entry.msgid][lang] = set(re.findall("{.*?}", entry.msgstr))
+1 -1
View File
@@ -109,6 +109,7 @@ nnMaxBatchSize = 96
# Other General GPU Settings------------------------------------------------------------------------------- # Other General GPU Settings-------------------------------------------------------------------------------
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree. # Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
nnCacheSizePowerOfTwo = 20 nnCacheSizePowerOfTwo = 20
# Size of mutex pool for nnCache is 2 ** this # Size of mutex pool for nnCache is 2 ** this
@@ -116,7 +117,6 @@ nnMutexPoolSizePowerOfTwo = 16
# Randomize board orientation when running neural net evals? # Randomize board orientation when running neural net evals?
nnRandomize = true nnRandomize = true
# TO USE MULTIPLE GPUS: # TO USE MULTIPLE GPUS:
# Set this to the number of GPUs you have and/or would like to use... # 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. # AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
Binary file not shown.
Binary file not shown.
+45 -22
View File
@@ -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("graphics", "height", max(400, int(WINDOW_Y * WINDOW_SCALE_FAC)))
Config.set("input", "mouse", "mouse,multitouch_on_demand") Config.set("input", "mouse", "mouse,multitouch_on_demand")
import re
import signal import signal
import sys import sys
import threading import threading
import traceback import traceback
from queue import Queue from queue import Queue
import urllib3
import webbrowser import webbrowser
from kivy.base import ExceptionHandler, ExceptionManager from kivy.base import ExceptionHandler, ExceptionManager
@@ -130,9 +132,9 @@ class KaTrainGui(Screen, KaTrainBase):
return self.play_mode.mode return self.play_mode.mode
def toggle_continuous_analysis(self): def toggle_continuous_analysis(self):
self.idle_analysis = not self.idle_analysis if self.idle_analysis:
if not self.idle_analysis:
self.controls.set_status("", STATUS_INFO) self.controls.set_status("", STATUS_INFO)
self.idle_analysis = not self.idle_analysis
self.update_state() self.update_state()
def start(self): 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.clear_widgets()
self.board_controls.mid_circles_container.add_widget(bot) self.board_controls.mid_circles_container.add_widget(bot)
self.board_controls.mid_circles_container.add_widget(top) 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["W"].captures = prisoners["W"]
self.controls.players["B"].captures = prisoners["B"] self.controls.players["B"].captures = prisoners["B"]
@@ -168,6 +169,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_controls.engine_status_col = ENGINE_READY_COL self.board_controls.engine_status_col = ENGINE_READY_COL
else: else:
self.board_controls.engine_status_col = ENGINE_BUSY_COL self.board_controls.engine_status_col = ENGINE_BUSY_COL
self.board_controls.queries_remaining = len(self.engine.queries)
# redraw board/stones # redraw board/stones
if redraw_board: if redraw_board:
@@ -193,21 +195,21 @@ class KaTrainGui(Screen, KaTrainBase):
teaching_undo = cn.player and last_player.being_taught and cn.parent teaching_undo = cn.player and last_player.being_taught and cn.parent
if ( if (
teaching_undo teaching_undo
and cn.analysis_ready and cn.analysis_complete
and cn.parent.analysis_ready and cn.parent.analysis_complete
and not cn.children and not cn.children
and not self.game.end_result and not self.game.end_result
): ):
self.game.analyze_undo(cn) # not via message loop self.game.analyze_undo(cn) # not via message loop
if ( if (
cn.analysis_ready cn.analysis_complete
and next_player.ai and next_player.ai
and not cn.children and not cn.children
and not self.game.end_result and not self.game.end_result
and not (teaching_undo and cn.auto_undo is None) and not (teaching_undo and cn.auto_undo is None)
): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly. ): # cn mismatch stops this if undo fired. avoid message loop here or fires repeatedly.
self._do_ai_move(cn) 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: if len(self.engine.queries) == 0 and self.idle_analysis:
self("analyze-extra", "extra", continuous=True) self("analyze-extra", "extra", continuous=True)
Clock.schedule_once(lambda _dt: self.update_gui(cn, redraw_board=redraw_board), -1) # trigger? 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.controls.update_players()
self.update_state() self.update_state()
for player_setup_block in PlayerSetupBlock.INSTANCES: 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): def set_note(self, note):
self.game.current_node.note = note self.game.current_node.note = note
@@ -293,6 +295,10 @@ class KaTrainGui(Screen, KaTrainBase):
self.board_gui.animating_pv = None self.board_gui.animating_pv = None
self.game.redo(n_times) 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): def _do_cycle_children(self, *args):
self.board_gui.animating_pv = None self.board_gui.animating_pv = None
self.game.cycle_children(*args) self.game.cycle_children(*args)
@@ -324,7 +330,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True self.controls.timer.paused = True
if not self.timer_settings_popup: if not self.timer_settings_popup:
self.timer_settings_popup = I18NPopup( 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__
self.timer_settings_popup.content.popup = self.timer_settings_popup self.timer_settings_popup.content.popup = self.timer_settings_popup
self.timer_settings_popup.open() self.timer_settings_popup.open()
@@ -351,12 +357,12 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True self.controls.timer.paused = True
if not self.ai_settings_popup: if not self.ai_settings_popup:
self.ai_settings_popup = I18NPopup( 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__
self.ai_settings_popup.content.popup = self.ai_settings_popup self.ai_settings_popup.content.popup = self.ai_settings_popup
self.ai_settings_popup.open() 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: try:
move_tree = KaTrainSGF.parse_file(file) move_tree = KaTrainSGF.parse_file(file)
except ParseError as e: except ParseError as e:
@@ -388,6 +394,7 @@ class KaTrainGui(Screen, KaTrainBase):
popup_contents.filesel.on_success = readfile popup_contents.filesel.on_success = readfile
popup_contents.filesel.on_submit = readfile popup_contents.filesel.on_submit = readfile
self.fileselect_popup.open() self.fileselect_popup.open()
self.fileselect_popup.content.filesel.ids.list_view._trigger_update()
def _do_output_sgf(self): def _do_output_sgf(self):
msg = self.game.write_sgf(self.config("general/sgf_save")) msg = self.game.write_sgf(self.config("general/sgf_save"))
@@ -399,6 +406,14 @@ class KaTrainGui(Screen, KaTrainBase):
if not clipboard: if not clipboard:
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_INFO) self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.", STATUS_INFO)
return return
url_match = re.match(r"(?P<url>https?://[^\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: try:
move_tree = KaTrainSGF.parse_sgf(clipboard) move_tree = KaTrainSGF.parse_sgf(clipboard)
except Exception as exc: except Exception as exc:
@@ -440,7 +455,9 @@ class KaTrainGui(Screen, KaTrainBase):
"s": ("analyze-extra", "equalize"), "s": ("analyze-extra", "equalize"),
"d": ("analyze-extra", "sweep"), "d": ("analyze-extra", "sweep"),
"f": ("analyze-extra", "alternative"), "f": ("analyze-extra", "alternative"),
"g": ("analyze-extra", "local"),
"p": ("play", None), "p": ("play", None),
"n": ("next-mistake",),
"down": ("switch-branch", 1), "down": ("switch-branch", 1),
"up": ("switch-branch", -1), "up": ("switch-branch", -1),
"f5": ("timer-popup",), "f5": ("timer-popup",),
@@ -466,7 +483,9 @@ class KaTrainGui(Screen, KaTrainBase):
return return
else: else:
return return
ctrl_pressed = "ctrl" in modifiers
alt_pressed = "alt" in modifiers
shift_pressed = "shift" in modifiers
shortcuts = self.shortcuts shortcuts = self.shortcuts
if keycode[1] == "tab": if keycode[1] == "tab":
self.play_mode.switch_ui_mode() self.play_mode.switch_ui_mode()
@@ -474,26 +493,30 @@ class KaTrainGui(Screen, KaTrainBase):
self.nav_drawer.set_state("toggle") self.nav_drawer.set_state("toggle")
elif keycode[1] == "spacebar": elif keycode[1] == "spacebar":
self.toggle_continuous_analysis() 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 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 self.zen = (self.zen + 1) % 3
elif keycode[1] in ["left", "z"]: 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"]: elif keycode[1] in ["right", "x"]:
self("redo", 1 + ("alt" in modifiers) * 9 + ("ctrl" in modifiers) * 999) self("redo", 1 + (alt_pressed or shift_pressed) * 9 + (ctrl_pressed and not alt_pressed) * 999)
elif keycode[1] == "n" and "ctrl" in modifiers: 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") self("new-game-popup")
elif keycode[1] == "l" and "ctrl" in modifiers: elif keycode[1] == "l" and ctrl_pressed:
self("analyze-sgf-popup") self("analyze-sgf-popup")
elif keycode[1] == "s" and "ctrl" in modifiers: elif keycode[1] == "s" and ctrl_pressed:
self("output-sgf") self("output-sgf")
elif keycode[1] == "c" and "ctrl" in modifiers: elif keycode[1] == "c" and ctrl_pressed:
Clipboard.copy(self.game.root.sgf()) Clipboard.copy(self.game.root.sgf())
self.controls.set_status(i18n._("Copied SGF to clipboard."), STATUS_INFO) 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() 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]] shortcut = shortcuts[keycode[1]]
if isinstance(shortcut, Widget): if isinstance(shortcut, Widget):
shortcut.trigger_action(duration=0) shortcut.trigger_action(duration=0)
+19 -1
View File
@@ -17,11 +17,13 @@
"anim_pv_time": 0.5, "anim_pv_time": 0.5,
"debug_level": 0, "debug_level": 0,
"lang": "en", "lang": "en",
"version": "1.5.0" "version": "1.6.0"
}, },
"timer": { "timer": {
"byo_length": 30, "byo_length": 30,
"byo_periods": 5, "byo_periods": 5,
"minimal_use": 0,
"main_time": 0,
"sound": true "sound": true
}, },
"game": { "game": {
@@ -86,6 +88,22 @@
"ai:policy": { "ai:policy": {
"opening_moves": 22.0 "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": { "ai:p:weighted": {
"weaken_fac": 1.25, "weaken_fac": 1.25,
"pick_override": 1.0, "pick_override": 1.0,
+93 -13
View File
@@ -8,29 +8,31 @@ from katrain.core.constants import (
AI_DEFAULT, AI_DEFAULT,
AI_HANDICAP, AI_HANDICAP,
AI_INFLUENCE, AI_INFLUENCE,
AI_INFLUENCE_ELO_GRID,
AI_JIGO, AI_JIGO,
AI_LOCAL, AI_LOCAL,
AI_LOCAL_ELO_GRID,
AI_PICK, AI_PICK,
AI_PICK_ELO_GRID,
AI_POLICY, AI_POLICY,
AI_RANK, AI_RANK,
AI_SCORELOSS, AI_SCORELOSS,
AI_SCORELOSS_ELO,
AI_SIMPLE_OWNERSHIP,
AI_STRATEGIES_PICK, AI_STRATEGIES_PICK,
AI_STRATEGIES_POLICY, AI_STRATEGIES_POLICY,
AI_STRENGTH, AI_STRENGTH,
AI_TENUKI, AI_TENUKI,
AI_TENUKI_ELO_GRID,
AI_TERRITORY, AI_TERRITORY,
AI_TERRITORY_ELO_GRID,
AI_WEIGHTED, AI_WEIGHTED,
AI_WEIGHTED_ELO,
CALIBRATED_RANK_ELO,
OUTPUT_DEBUG, OUTPUT_DEBUG,
OUTPUT_ERROR, OUTPUT_ERROR,
OUTPUT_INFO, OUTPUT_INFO,
AI_WEIGHTED_ELO, AI_SETTLE_STONES,
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,
) )
from katrain.core.game import Game, GameNode, Move from katrain.core.game import Game, GameNode, Move
from katrain.core.utils import var_to_grid 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 error = False
analysis = None analysis = None
def set_analysis(a): def set_analysis(a, partial_result):
nonlocal analysis nonlocal analysis
analysis = a if not partial_result:
analysis = a
def set_error(a): def set_error(a):
nonlocal error nonlocal error
game.katrain.log("Error in PDA-based analysis", a) game.katrain.log(f"Error in additional analysis query: {a}")
error = True error = True
engine = game.engines[cn.player] 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) game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT ai_mode = AI_DEFAULT
while not cn.analysis_ready: while not cn.analysis_complete:
time.sleep(0.01) time.sleep(0.01)
game.engines[cn.next_player].check_alive(exception_if_dead=True) 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"] candidate_ai_moves = handicap_analysis["moveInfos"]
top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) 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 aimove = top_cand
ai_thoughts += f"Top move is pass, so passing regardless of strategy. " ai_thoughts += f"Top move is pass, so passing regardless of strategy. "
else: 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] topmove = weighted_selection_without_replacement(moves, 1)[0]
aimove = topmove[2] 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." 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: else:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP]: 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) game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
+3 -3
View File
@@ -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""" """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): def __init__(self, force_package_config=False, debug_level=None, **kwargs):
self.debug_level = debug_level self.debug_level = debug_level or 0
self.game = None self.game = None
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
self.config_file = self._load_config(force_package_config=force_package_config) 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") Config.set("kivy", "log_level", "warning")
if self.debug_level >= OUTPUT_DEBUG: if self.debug_level >= OUTPUT_DEBUG:
+25 -4
View File
@@ -1,6 +1,6 @@
VERSION = "1.5.0" VERSION = "1.6.0"
HOMEPAGE = "https://github.com/sanderland/katrain" 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_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5 OUTPUT_KATAGO_STDERR = -0.5
@@ -33,10 +33,12 @@ AI_TENUKI = "ai:p:tenuki"
AI_INFLUENCE = "ai:p:influence" AI_INFLUENCE = "ai:p:influence"
AI_TERRITORY = "ai:p:territory" AI_TERRITORY = "ai:p:territory"
AI_RANK = "ai:p:rank" AI_RANK = "ai:p:rank"
AI_SIMPLE_OWNERSHIP = "ai:simple"
AI_SETTLE_STONES = "ai:settle"
AI_CONFIG_DEFAULT = AI_RANK 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_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_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK
AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY
@@ -44,6 +46,8 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_DEFAULT, AI_DEFAULT,
AI_RANK, AI_RANK,
AI_HANDICAP, AI_HANDICAP,
AI_SIMPLE_OWNERSHIP,
# AI_SETTLE_STONES,
AI_SCORELOSS, AI_SCORELOSS,
AI_POLICY, AI_POLICY,
AI_WEIGHTED, AI_WEIGHTED,
@@ -67,6 +71,8 @@ AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some?
AI_INFLUENCE: -7, AI_INFLUENCE: -7,
AI_TERRITORY: -7, AI_TERRITORY: -7,
AI_RANK: float("nan"), AI_RANK: float("nan"),
AI_SIMPLE_OWNERSHIP: 2,
AI_SETTLE_STONES: 2,
} }
AI_OPTION_VALUES = { AI_OPTION_VALUES = {
@@ -85,8 +91,23 @@ AI_OPTION_VALUES = {
"threshold": [2, 2.5, 3, 3.5, 4, 4.5], "threshold": [2, 2.5, 3, 3.5, 4, 4.5],
"automatic": "bool", "automatic": "bool",
"pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)], "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 = [ CALIBRATED_RANK_ELO = [
+47 -10
View File
@@ -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.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
from katrain.core.game_node import GameNode from katrain.core.game_node import GameNode
from katrain.core.lang import i18n from katrain.core.lang import i18n
from katrain.core.sgf_parser import Move
from katrain.core.utils import find_package_resource from katrain.core.utils import find_package_resource
@@ -85,10 +86,10 @@ class KataGoEngine:
def start(self): def start(self):
try: try:
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG) 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"): if hasattr(subprocess, "STARTUPINFO"):
startupinfo = 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.katago_process = subprocess.Popen(
self.command, self.command,
startupinfo=startupinfo, startupinfo=startupinfo,
@@ -107,6 +108,8 @@ class KataGoEngine:
def on_new_game(self): def on_new_game(self):
self.base_priority += 1 self.base_priority += 1
for query_id in list(self.queries.keys()):
self.terminate_query(query_id)
self.queries = {} self.queries = {}
def restart(self): def restart(self):
@@ -131,11 +134,14 @@ class KataGoEngine:
raise EngineDiedException(died_msg) raise EngineDiedException(died_msg)
return ok 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): def shutdown(self, finish=False):
process = self.katago_process process = self.katago_process
if finish and process: if finish and process:
while self.queries and process.poll() is None: self.wait_to_finish()
time.sleep(0.1)
if process: if process:
self.katago_process = None self.katago_process = None
process.terminate() process.terminate()
@@ -196,14 +202,18 @@ class KataGoEngine:
elif "warning" in analysis: elif "warning" in analysis:
self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG) self.katrain.log(f"{analysis} received from KataGo", OUTPUT_DEBUG)
else: 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 time_taken = time.time() - start_time
self.katrain.log( 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) self.katrain.log(line, OUTPUT_EXTRA_DEBUG)
try: try:
callback(analysis) if callback and not analysis.get("noResults", False):
callback(analysis, partial_result)
except Exception as e: except Exception as e:
self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR) 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 if getattr(self.katrain, "update_state", None): # easier mocking etc
@@ -225,7 +235,10 @@ class KataGoEngine:
self.katago_process.stdin.flush() self.katago_process.stdin.flush()
except OSError as e: except OSError as e:
self.check_alive(os_error=str(e), exception_if_dead=True) 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( def request_analysis(
self, self,
@@ -236,10 +249,12 @@ class KataGoEngine:
analyze_fast: bool = False, analyze_fast: bool = False,
time_limit=True, time_limit=True,
find_alternatives: bool = False, find_alternatives: bool = False,
find_local: bool = False,
priority: int = 0, priority: int = 0,
ownership: Optional[bool] = None, ownership: Optional[bool] = None,
next_move: Optional[GameNode] = None, next_move: Optional[GameNode] = None,
extra_settings: Optional[Dict] = None, extra_settings: Optional[Dict] = None,
report_every: Optional[float] = None,
): ):
nodes = analysis_node.nodes_from_root nodes = analysis_node.nodes_from_root
moves = [m for node in nodes for m in node.moves] 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"): if analyze_fast and self.config.get("fast_visits"):
visits = self.config["fast_visits"] visits = self.config["fast_visits"]
size_x, size_y = analysis_node.board_size
if find_alternatives: if find_alternatives:
avoid = [ avoid = [
{ {
@@ -261,10 +278,26 @@ class KataGoEngine:
"untilDepth": 1, "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: else:
avoid = [] avoid = []
size_x, size_y = analysis_node.board_size
settings = copy.copy(self.override_settings) settings = copy.copy(self.override_settings)
if time_limit: if time_limit:
settings["maxTime"] = self.config["max_time"] settings["maxTime"] = self.config["max_time"]
@@ -277,15 +310,19 @@ class KataGoEngine:
"analyzeTurns": [len(moves)], "analyzeTurns": [len(moves)],
"maxVisits": visits, "maxVisits": visits,
"komi": analysis_node.komi, "komi": analysis_node.komi,
"avoidMoves": avoid,
"boardXSize": size_x, "boardXSize": size_x,
"boardYSize": size_y, "boardYSize": size_y,
"includeOwnership": ownership and not next_move, "includeOwnership": ownership and not next_move,
"includeMovesOwnership": ownership and not next_move,
"includePolicy": not next_move, "includePolicy": not next_move,
"initialStones": [[m.player, m.gtp()] for m in initial_stones], "initialStones": [[m.player, m.gtp()] for m in initial_stones],
"initialPlayer": analysis_node.root.next_player, "initialPlayer": analysis_node.root.next_player,
"moves": [[m.player, m.gtp()] for m in moves], "moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": {**settings, **(extra_settings or {})}, "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) self.send_query(query, callback, error_callback, next_move)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits) analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits)
+30 -14
View File
@@ -79,12 +79,14 @@ class Game:
self.root.set_property("RU", katrain.config("game/rules")) self.root.set_property("RU", katrain.config("game/rules"))
self.set_current_node(self.root) self.set_current_node(self.root)
self.main_time_used = 0
threading.Thread( threading.Thread(
target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True
).start() # return faster, but bypass Kivy Clock ).start() # return faster, but bypass Kivy Clock
def analyze_all_nodes(self, priority=0, analyze_fast=False): def analyze_all_nodes(self, priority=0, analyze_fast=False):
for node in self.root.nodes_in_tree: for node in self.root.nodes_in_tree:
node.clear_analysis()
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast) node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
# -- move tree functions -- # -- move tree functions --
@@ -183,12 +185,22 @@ class Game:
cn = cn.parent cn = cn.parent
self.set_current_node(cn) 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 cn = self.current_node # avoid race conditions
for _ in range(n_times): for move in range(n_times):
if cn.children: if cn.children:
cn = cn.ordered_children[0] 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): def cycle_children(self, direction):
cn = self.current_node # avoid race conditions cn = self.current_node # avoid race conditions
@@ -327,6 +339,7 @@ class Game:
) )
else: else:
visits = cn.analysis_visits_requested + engine.config["max_visits"] 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) self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False) cn.analyze(engine, visits=visits, priority=-1_000, time_limit=False)
return return
@@ -338,13 +351,13 @@ class Game:
min_visits = min(node.analysis_visits_requested for node in nodes) min_visits = min(node.analysis_visits_requested for node in nodes)
visits = min_visits + engine.config["max_visits"] visits = min_visits + engine.config["max_visits"]
for node in nodes: 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) self.katrain.controls.set_status(i18n._("game re-analysis").format(visits=visits), STATUS_ANALYSIS)
return return
elif mode == "sweep": elif mode == "sweep":
board_size_x, board_size_y = self.board_size board_size_x, board_size_y = self.board_size
if cn.analysis_ready: if cn.analysis_exists:
policy_grid = ( policy_grid = (
var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y))
if self.current_node.policy if self.current_node.policy
@@ -369,32 +382,35 @@ class Game:
visits = engine.config["fast_visits"] visits = engine.config["fast_visits"]
self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS) self.katrain.controls.set_status(i18n._("sweep analysis").format(visits=visits), STATUS_ANALYSIS)
priority = -1_000_000_000 priority = -1_000_000_000
elif mode in ["equalize", "alternative"]: elif mode == "local": # also do a quick update on current candidates so it doesn't look too weird
if not cn.analysis_ready: 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) self.katrain.controls.set_status(i18n._("wait-before-equalize"), STATUS_INFO, self.current_node)
return 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 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) 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"] visits = engine.config["fast_visits"]
else: else: # equalize
visits = max(d["visits"] for d in cn.analysis["moves"].values()) visits = max(d["visits"] for d in cn.analysis["moves"].values())
self.katrain.controls.set_status(i18n._("equalizing analysis").format(visits=visits), STATUS_ANALYSIS) 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: else:
raise ValueError("Invalid analysis mode") raise ValueError("Invalid analysis mode")
for move in analyze_moves: for move in analyze_moves:
if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits: if cn.analysis["moves"].get(move.gtp(), {"visits": 0})["visits"] < visits:
cn.analyze( 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 ) # explicitly requested so take as long as you need
def analyze_undo(self, node): def analyze_undo(self, node):
train_config = self.katrain.config("trainer") train_config = self.katrain.config("trainer")
move = node.move 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 return
points_lost = node.points_lost points_lost = node.points_lost
thresholds = train_config["eval_thresholds"] thresholds = train_config["eval_thresholds"]
+41 -23
View File
@@ -13,7 +13,6 @@ class GameNode(SGFNode):
def __init__(self, parent=None, properties=None, move=None): def __init__(self, parent=None, properties=None, move=None):
super().__init__(parent=parent, properties=properties, move=move) super().__init__(parent=parent, properties=properties, move=move)
self.analysis = {"moves": {}, "root": None}
self.ownership = None self.ownership = None
self.policy = None self.policy = None
self.auto_undo = None # None = not analyzed. False: not undone (good move). True: undone (bad move) 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.note = ""
self.move_number = 0 self.move_number = 0
self.time_used = 0 self.time_used = 0
self.analysis_visits_requested = 0
self.undo_threshold = random.random() # for fractional undos self.undo_threshold = random.random() # for fractional undos
self.end_state = None 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): def sgf_properties(self, save_comments_player=None, save_comments_class=None, eval_thresholds=None):
properties = copy.copy(super().sgf_properties()) properties = copy.copy(super().sgf_properties())
@@ -34,8 +37,8 @@ class GameNode(SGFNode):
show_class = False show_class = False
if ( if (
self.parent self.parent
and self.parent.analysis_ready and self.parent.analysis_exists
and self.analysis_ready and self.analysis_exists
and (note or ((save_comments_player or {}).get(self.player, False) and show_class)) and (note or ((save_comments_player or {}).get(self.player, False) and show_class))
): ):
candidate_moves = self.parent.candidate_moves candidate_moves = self.parent.candidate_moves
@@ -77,16 +80,22 @@ class GameNode(SGFNode):
refine_move=None, refine_move=None,
analyze_fast=False, analyze_fast=False,
find_alternatives=False, find_alternatives=False,
find_local=False,
report_every=0.25,
): ):
engine.request_analysis( engine.request_analysis(
self, 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, priority=priority,
visits=visits, visits=visits,
analyze_fast=analyze_fast, analyze_fast=analyze_fast,
time_limit=time_limit, time_limit=time_limit,
next_move=refine_move, next_move=refine_move,
find_alternatives=find_alternatives, find_alternatives=find_alternatives,
find_local=find_local,
report_every=report_every,
) )
def update_move_analysis(self, move_analysis, move_gtp): def update_move_analysis(self, move_analysis, move_gtp):
@@ -102,24 +111,26 @@ class GameNode(SGFNode):
if cur["visits"] < move_analysis["visits"]: if cur["visits"] < move_analysis["visits"]:
cur.update(move_analysis) 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: if refine_move:
pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else [] pvtail = analysis_json["moveInfos"][0]["pv"] if analysis_json["moveInfos"] else []
self.update_move_analysis( self.update_move_analysis(
{"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp() {"pv": [refine_move.gtp()] + pvtail, **analysis_json["rootInfo"]}, refine_move.gtp()
) )
else: else:
if alternatives_mode: if additional_moves:
for m in analysis_json["moveInfos"]: for m in analysis_json["moveInfos"]:
m["order"] += 10 # offset for not making this top del m["order"] # avoid changing order
if refine_move is None and not alternatives_mode: if refine_move is None and not additional_moves:
for move_dict in self.analysis["moves"].values(): for move_dict in self.analysis["moves"].values():
move_dict["order"] = 999 # old moves to end move_dict["order"] = 999 # old moves to end
for move_analysis in analysis_json["moveInfos"]: for move_analysis in analysis_json["moveInfos"]:
self.update_move_analysis(move_analysis, move_analysis["move"]) self.update_move_analysis(move_analysis, move_analysis["move"])
self.ownership = analysis_json.get("ownership") self.ownership = analysis_json.get("ownership")
self.policy = analysis_json.get("policy") self.policy = analysis_json.get("policy")
if not alternatives_mode: if not additional_moves:
self.analysis["root"] = analysis_json["rootInfo"] self.analysis["root"] = analysis_json["rootInfo"]
if self.parent and self.move: if self.parent and self.move:
analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + ( analysis_json["rootInfo"]["pv"] = [self.move.gtp()] + (
@@ -128,14 +139,20 @@ class GameNode(SGFNode):
self.parent.update_move_analysis( self.parent.update_move_analysis(
analysis_json["rootInfo"], self.move.gtp() analysis_json["rootInfo"], self.move.gtp()
) # update analysis in parent for consistency ) # 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 @property
def analysis_ready(self): def analysis_exists(self):
return self.analysis["root"] is not None return self.analysis["root"] is not None
@property
def analysis_complete(self):
return self.analysis["completed"] and self.analysis["root"] is not None
@property @property
def score(self) -> Optional[float]: def score(self) -> Optional[float]:
if self.analysis_ready: if self.analysis_exists:
return self.analysis["root"].get("scoreLead") return self.analysis["root"].get("scoreLead")
def format_score(self, score=None): def format_score(self, score=None):
@@ -145,7 +162,7 @@ class GameNode(SGFNode):
@property @property
def winrate(self) -> Optional[float]: def winrate(self) -> Optional[float]:
if self.analysis_ready: if self.analysis_exists:
return self.analysis["root"].get("winrate") return self.analysis["root"].get("winrate")
def format_winrate(self, win_rate=None): def format_winrate(self, win_rate=None):
@@ -176,12 +193,12 @@ class GameNode(SGFNode):
return "" return ""
text = i18n._("move").format(number=self.depth) + f": {single_move.player} {single_move.gtp()}\n" 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 score = self.score
if sgf: if sgf:
text += i18n._("Info:score").format(score=self.format_score(score)) + "\n" text += i18n._("Info:score").format(score=self.format_score(score)) + "\n"
text += i18n._("Info:winrate").format(winrate=self.format_winrate()) + "\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] previous_top_move = self.parent.candidate_moves[0]
if sgf or details: if sgf or details:
if previous_top_move["move"] != single_move.gtp(): if previous_top_move["move"] != single_move.gtp():
@@ -202,20 +219,21 @@ class GameNode(SGFNode):
if currmove_pol_rank is not None: if currmove_pol_rank is not None:
policy_rank_msg = i18n._("Info:policy rank") policy_rank_msg = i18n._("Info:policy rank")
text += policy_rank_msg.format(rank=currmove_pol_rank, probability=currmove_pol_prob) + "\n" 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") policy_best_msg = i18n._("Info:policy best")
pol_move, pol_prob = policy_ranking[0][1].gtp(), policy_ranking[0][0] 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" text += policy_best_msg.format(move=pol_move, probability=pol_prob) + "\n"
if self.auto_undo and sgf: if self.auto_undo and sgf:
text += i18n._("Info:teaching undo") + "\n" 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: if top_pv:
text += i18n._("Info:undo predicted PV").format(pv=f"{self.next_player}{' '.join(top_pv)}") + "\n" 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: else:
text = i18n._("No analysis available") if sgf else i18n._("Analyzing move...") 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: if "C" in self.properties:
text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"]) text += "\n[u]SGF Comments:[/u]\n" + "\n".join(self.properties["C"])
@@ -224,7 +242,7 @@ class GameNode(SGFNode):
@property @property
def points_lost(self) -> Optional[float]: def points_lost(self) -> Optional[float]:
single_move = self.move 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 parent_score = self.parent.score
score = self.score score = self.score
return self.player_sign(single_move.player) * (parent_score - score) return self.player_sign(single_move.player) * (parent_score - score)
@@ -236,8 +254,8 @@ class GameNode(SGFNode):
single_move single_move
and self.parent and self.parent
and self.parent.parent and self.parent.parent
and self.analysis_ready and self.analysis_exists
and self.parent.parent.analysis_ready and self.parent.parent.analysis_exists
): ):
parent_parent_score = self.parent.parent.score parent_parent_score = self.parent.parent.score
score = self.score score = self.score
@@ -249,7 +267,7 @@ class GameNode(SGFNode):
@property @property
def candidate_moves(self) -> List[Dict]: def candidate_moves(self) -> List[Dict]:
if not self.analysis_ready: if not self.analysis_exists:
return [] return []
if not self.analysis["moves"]: if not self.analysis["moves"]:
polmoves = self.policy_ranking polmoves = self.policy_ranking
+33 -25
View File
@@ -32,7 +32,7 @@
#:set CP_SPACING dp(6) #:set CP_SPACING dp(6)
#:set CP_SMALL_SPACING dp(3) #:set CP_SMALL_SPACING dp(3)
#:set CP_PADDING dp(6) #:set CP_PADDING dp(6)
#:set NOTES_FONT_SIZE dp(18) #:set NOTES_FONT_SIZE sp(18)
#:set EPSILON 1e-9 #: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]] pos: [root.pos[i] + (root.size[i] - root.icon_size[i])/2 for i in [0,1]]
source: root.icon source: root.icon
mipmap: True 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
<I18NSpinnerOption@SpinnerOption>: <I18NSpinnerOption@SpinnerOption>:
@@ -171,8 +171,6 @@
size: image.size size: image.size
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text)) font_size: root.min_size * 1.2 / (1 + 1 * len(self.text))
# UI components # UI components
<BadukPanWidget>: <BadukPanWidget>:
@@ -187,7 +185,6 @@
<BadukPanControls>: <BadukPanControls>:
katrain: app.gui katrain: app.gui
branch: branch
circles: [black_circle,white_circle] circles: [black_circle,white_circle]
mid_circles_container: mid_circles_container mid_circles_container: mid_circles_container
pass_btn: pass_btn pass_btn: pass_btn
@@ -209,7 +206,7 @@
on_press: root.katrain('undo',999) on_press: root.katrain('undo',999)
NavIconButton: NavIconButton:
icon: 'img/Previous-5.png' icon: 'img/Previous-5.png'
on_press: root.katrain('undo',5) on_press: root.katrain('undo',10)
NavIconButton: NavIconButton:
icon: 'img/Previous.png' icon: 'img/Previous.png'
on_press: root.katrain('undo') on_press: root.katrain('undo')
@@ -233,20 +230,19 @@
on_press: root.katrain('redo') on_press: root.katrain('redo')
NavIconButton: NavIconButton:
icon: 'img/Next-5.png' icon: 'img/Next-5.png'
on_press: root.katrain('redo',5) on_press: root.katrain('redo',10)
NavIconButton: NavIconButton:
icon: 'img/Next-End.png' icon: 'img/Next-End.png'
on_press: root.katrain('redo',999) on_press: root.katrain('redo',999)
NavIconButton: NavIconButton:
id: branch icon: 'img/Next-Mistake.png'
icon: 'img/Branch.png' color: [0.65,0.05,0.05,1]
on_press: root.katrain('cycle-children',1) on_press: root.katrain('next-mistake')
disabled: True
ClickableLabel: ClickableLabel:
pos_hint: {'center_x': 0.85,'center_y':0.5} pos_hint: {'center_x': 0.85,'center_y':0.5}
size_hint: None, 1 size_hint: None, 1
width: self.height width: self.height
canvas: canvas.before:
Color: Color:
rgba: YELLOW rgba: YELLOW
Ellipse: Ellipse:
@@ -264,6 +260,9 @@
Line: Line:
circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, self.width/6 circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, self.width/6
width: 1 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) on_press: app.gui.controls.set_status(i18n._('ai-ponder'),STATUS_INFO)
@@ -357,14 +356,14 @@
spacing: CP_SPACING spacing: CP_SPACING
padding: CP_PADDING * 4, CP_PADDING * 2 padding: CP_PADDING * 4, CP_PADDING * 2
paused: True paused: True
timeout: self.state[1] <= 0 timeout: root.state[1] is not None and root.state[1] <= 0
MDBoxLayout: MDBoxLayout:
size_hint: 0.7,1 size_hint: 0.7,1
MDFloatLayout: MDFloatLayout:
size_hint: 0.4,1 size_hint: 0.4,1
TimerLabel: TimerLabel:
color: BACKGROUND_COLOR color: BACKGROUND_COLOR
text: '8:88' text: '{}:88'.format('188' if root.state[0] >= 6000 else '88')
font_size: 0.85*self.height font_size: 0.85*self.height
pos_hint: {'right':1,'center_y':0.5} pos_hint: {'right':1,'center_y':0.5}
halign: 'right' halign: 'right'
@@ -372,27 +371,27 @@
TimerLabel: TimerLabel:
id: timer id: timer
timeout: root.timeout 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 font_size: 0.85*self.height
pos_hint: {'right':1,'center_y':0.5} pos_hint: {'right':1,'center_y':0.5}
halign: 'right' halign: 'right'
valign: 'middle' valign: 'middle'
TimerLabel: TimerLabel:
size_hint: 0.05,1 size_hint: 0.05,1
text: "x" text: "x" if root.state[1] is not None else ""
font_size: self.height * 0.3 font_size: self.height * 0.3
text_size: self.width, self.height * 0.95 text_size: self.width, self.height * 0.95
halign: 'right' halign: 'right'
valign: 'top' valign: 'top'
timeout: root.timeout timeout: root.timeout
TimerLabel: TimerLabel:
size_hint: 0.1,1 size_hint: 0.075,1
font_size: self.height * 0.35 font_size: self.height * 0.35
text_size: self.size text_size: self.size
halign: 'left' halign: 'left'
valign: 'top' valign: 'top'
id: periods id: periods
text: str(root.state[1]) text: str(root.state[1]) if root.state[1] is not None else ""
max_lines: 1 max_lines: 1
timeout: root.timeout timeout: root.timeout
MDBoxLayout: MDBoxLayout:
@@ -507,8 +506,6 @@
katrain: app.gui katrain: app.gui
orientation: 'vertical' orientation: 'vertical'
players: {'B':B_player,'W':W_player} players: {'B':B_player,'W':W_player}
engine_status_col: [1,1,1,1]
engine_status_pondering: -1
timer:timer timer:timer
graph: graph graph: graph
stats: stats stats: stats
@@ -685,6 +682,7 @@
color: LIGHTGREY color: LIGHTGREY
<AnalysisToggle>: <AnalysisToggle>:
adaptive_width: True adaptive_width: True
spacing: CP_SPACING 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) color: checkbox.disabled_color if root.disabled else (checkbox.selected_color if checkbox.active else checkbox.unselected_color)
size_hint_x: None size_hint_x: None
width: self.texture_size[0] + 4 width: self.texture_size[0] + 4
on_left_press: checkbox.active = not checkbox.active on_left_press: checkbox.trigger_action()
MDCheckbox: TriStateMDCheckbox:
id: checkbox id: checkbox
disabled: root.disabled disabled: root.disabled
tri_state: root.tri_state
size_hint: None,0.5 size_hint: None,0.5
pos_hint: {'center_y': 0.5} pos_hint: {'center_y': 0.5}
width: self.height width: self.height
active: root.default_active active: root.default_active
on_active: if app.gui: app.gui.update_state() on_active: if app.gui: app.gui.update_state()
on_slashed: if app.gui: app.gui.update_state()
<AnalysisMenuItem@MenuItem>: <AnalysisMenuItem@MenuItem>:
on_close: app.gui.analysis_controls.close_dropdown() on_close: app.gui.analysis_controls.close_dropdown()
@@ -749,6 +750,11 @@
icon: 'img/Alternative.png' icon: 'img/Alternative.png'
shortcut: 'f' shortcut: 'f'
on_action: root.katrain("analyze-extra", "alternative") 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: AnalysisMenuItem:
text: i18n._("analysis:game") text: i18n._("analysis:game")
icon: 'img/analysis.png' icon: 'img/analysis.png'
@@ -792,6 +798,7 @@
AnalysisToggle: AnalysisToggle:
id: eval id: eval
text: i18n._("analysis:dots") text: i18n._("analysis:dots")
tri_state: True
AnalysisToggle: AnalysisToggle:
id: hints id: hints
text: i18n._("analysis:topmoves") text: i18n._("analysis:topmoves")
@@ -853,11 +860,12 @@
player_subtype_ai: player_subtype_ai player_subtype_ai: player_subtype_ai
MDBoxLayout: MDBoxLayout:
size_hint: 0.25, 1 size_hint: 0.25, 1
CircleWithText: ClickableCircle:
size_hint: 0.8, 0.8 size_hint: 0.8, 0.8
pos_hint: {'center_x':0.5,'center_y':0.5} pos_hint: {'center_x':0.5,'center_y':0.5}
text: root.player[0] text: root.player[0]
player: root.player player: root.player
on_left_press: root.parent.swap_players()
MDBoxLayout: MDBoxLayout:
size_hint: 0.5, 1 size_hint: 0.5, 1
orientation: 'vertical' orientation: 'vertical'
@@ -1059,14 +1067,14 @@
AnalysisControls: AnalysisControls:
id: analysis_controls id: analysis_controls
size_hint_y: None 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 opacity: 0 if root.zen > 0 else 1
BadukPanWidget: BadukPanWidget:
id: board_gui id: board_gui
BadukPanControls: BadukPanControls:
id: board_controls id: board_controls
size_hint_y: None 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 opacity: 0 if root.zen > 0 else 1
MDBoxLayout: MDBoxLayout:
size_hint_x: None size_hint_x: None
+26 -7
View File
@@ -42,7 +42,7 @@ class BadukPanWidget(Widget):
Window.bind(mouse_pos=self.on_mouse_pos) Window.bind(mouse_pos=self.on_mouse_pos)
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents) self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents)
self.redraw_trigger = Clock.create_trigger(self.redraw) 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) Clock.schedule_interval(self.animate_pv, 0.1)
# stone placement functions # stone placement functions
@@ -106,8 +106,23 @@ class BadukPanWidget(Widget):
return return
katrain = self.katrain katrain = self.katrain
if self.ghost_stone and ("button" not in touch.profile or touch.button == "left"): if self.ghost_stone and ("button" not in touch.profile or touch.button == "left"):
katrain("play", self.ghost_stone) game = self.katrain and self.katrain.game
self.play_stone_sound() 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: elif not self.ghost_stone:
xd, xp = self._find_closest(touch.x, self.gridpos_x) xd, xp = self._find_closest(touch.x, self.gridpos_x)
yd, yp = self._find_closest(touch.y, self.gridpos_y) 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.log(f"\nRoot Stats:\n{nodes_here[-1].analysis['root']}", OUTPUT_DEBUG)
katrain.controls.info.text = nodes_here[-1].comment(sgf=True) katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
katrain.controls.active_comment_node = nodes_here[-1] 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.set_animating_pv(nodes_here[-1].parent.candidate_moves[0]["pv"], nodes_here[-1].parent)
self.ghost_stone = None self.ghost_stone = None
@@ -257,6 +272,7 @@ class BadukPanWidget(Widget):
current_node = katrain.game.current_node current_node = katrain.game.current_node
game_ended = katrain.game.end_result game_ended = katrain.game.end_result
full_eval_on = katrain.analysis_controls.eval.active full_eval_on = katrain.analysis_controls.eval.active
all_dots_off = katrain.analysis_controls.eval.checkbox.slashed
has_stone = {} has_stone = {}
drawn_stone = {} drawn_stone = {}
for m in katrain.game.stones: for m in katrain.game.stones:
@@ -280,7 +296,9 @@ class BadukPanWidget(Widget):
placements = node.placements placements = node.placements
for m in node.moves + 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 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: if move_eval_on and points_lost is not None:
evalcol = self.eval_color(points_lost, show_dots_for_class) evalcol = self.eval_color(points_lost, show_dots_for_class)
else: else:
@@ -454,7 +472,7 @@ class BadukPanWidget(Widget):
source="img/topmove.png", source="img/topmove.png",
) )
if self.trainer_config["text_point_loss"] and text_on: 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" ptloss_text = "0.0"
else: else:
ptloss_text = f"{-move_dict['pointsLost']:+.1f}" ptloss_text = f"{-move_dict['pointsLost']:+.1f}"
@@ -484,7 +502,7 @@ class BadukPanWidget(Widget):
for child_node in current_node.children: for child_node in current_node.children:
move = child_node.move move = child_node.move
if move and move.coords is not None: if move and move.coords is not None:
if child_node.analysis_ready: if child_node.analysis_exists:
self.active_pv_moves.append( self.active_pv_moves.append(
(move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node) (move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node)
) )
@@ -619,3 +637,4 @@ class AnalysisControls(MDBoxLayout):
class BadukPanControls(MDFloatLayout): class BadukPanControls(MDFloatLayout):
engine_status_col = ListProperty(ENGINE_DOWN_COL) engine_status_col = ListProperty(ENGINE_DOWN_COL)
engine_status_pondering = NumericProperty(-1) engine_status_pondering = NumericProperty(-1)
queries_remaining = NumericProperty(0)
+32 -5
View File
@@ -71,6 +71,8 @@ class ControlsPanel(BoxLayout):
self.active_comment_node = None self.active_comment_node = None
self.last_timer_update = (None, 0, False) self.last_timer_update = (None, 0, False)
self.beep = SoundLoader.load("sounds/countdownbeep.wav") self.beep = SoundLoader.load("sounds/countdownbeep.wav")
self.boing = SoundLoader.load("sounds/boing.wav")
self.boing.volume = 0.1
self.beep_start = 5.2 self.beep_start = 5.2
self.timer_interval = 0.07 self.timer_interval = 0.07
@@ -121,7 +123,7 @@ class ControlsPanel(BoxLayout):
self.active_comment_node = current_node.children[-1] self.active_comment_node = current_node.children[-1]
elif current_node.parent: elif current_node.parent:
self.active_comment_node = 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 self.active_comment_node = current_node.parent
lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY lock_ai = katrain.config("trainer/lock_ai") and katrain.play_analyze_mode == MODE_PLAY
@@ -132,7 +134,7 @@ class ControlsPanel(BoxLayout):
teach=katrain.players_info[self.active_comment_node.player].being_taught, details=details 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.score = self.active_comment_node.format_score() or ""
self.stats.winrate = self.active_comment_node.format_winrate() or "" self.stats.winrate = self.active_comment_node.format_winrate() or ""
self.stats.points_lost = self.active_comment_node.points_lost self.stats.points_lost = self.active_comment_node.points_lost
@@ -149,11 +151,13 @@ class ControlsPanel(BoxLayout):
self.info.text = info self.info.text = info
def update_timer(self, _dt): 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: if current_node:
last_update_node, last_update_time, beeping = self.last_timer_update last_update_node, last_update_time, beeping = self.last_timer_update
new_beeping = beeping new_beeping = beeping
now = time.time() now = time.time()
main_time = self.katrain.config("timer/main_time", 0) * 60
byo_len = max(1, self.katrain.config("timer/byo_length")) byo_len = max(1, self.katrain.config("timer/byo_length"))
byo_num = max(1, self.katrain.config("timer/byo_periods")) byo_num = max(1, self.katrain.config("timer/byo_periods"))
sounds_on = self.katrain.config("timer/sound") sounds_on = self.katrain.config("timer/sound")
@@ -161,9 +165,16 @@ class ControlsPanel(BoxLayout):
ai = player.ai ai = player.ai
used_period = False 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 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: 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: else:
current_node.time_used = 0 current_node.time_used = 0
new_beeping = False new_beeping = False
@@ -173,6 +184,7 @@ class ControlsPanel(BoxLayout):
time_remaining += byo_len time_remaining += byo_len
player.periods_used += 1 player.periods_used += 1
used_period = True used_period = True
if ( if (
self.beep_start - 2 * self.timer_interval < time_remaining < self.beep_start self.beep_start - 2 * self.timer_interval < time_remaining < self.beep_start
and player.periods_used < byo_num and player.periods_used < byo_num
@@ -180,6 +192,18 @@ class ControlsPanel(BoxLayout):
new_beeping = True new_beeping = True
elif time_remaining > self.beep_start: elif time_remaining > self.beep_start:
new_beeping = False 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: else:
new_beeping = False new_beeping = False
@@ -198,4 +222,7 @@ class ControlsPanel(BoxLayout):
self.last_timer_update = (current_node, now, new_beeping) 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)
+34 -3
View File
@@ -23,6 +23,7 @@ from kivymd.uix.behaviors import CircularRippleBehavior, RectangularRippleBehavi
from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import BaseFlatButton, BasePressedButton from kivymd.uix.button import BaseFlatButton, BasePressedButton
from kivymd.uix.navigationdrawer import MDNavigationDrawer from kivymd.uix.navigationdrawer import MDNavigationDrawer
from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField from kivymd.uix.textfield import MDTextField
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, MODE_PLAY, PLAYER_AI 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): class TransparentIconButton(CircularRippleBehavior, Button):
color = ListProperty([1, 1, 1, 1])
icon_size = ListProperty([25, 25]) icon_size = ListProperty([25, 25])
icon = StringProperty("") icon = StringProperty("")
disabled = BooleanProperty(False) disabled = BooleanProperty(False)
@@ -342,9 +344,12 @@ class PlayerSetupBlock(MDBoxLayout):
def swap_players(self): def swap_players(self):
player_dump = {bw: p.player_type_dump for bw, p in self.players.items()} player_dump = {bw: p.player_type_dump for bw, p in self.players.items()}
for bw in "BW": 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) 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) 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): class AnalysisToggle(MDBoxLayout):
text = StringProperty("") text = StringProperty("")
default_active = BooleanProperty(False) default_active = BooleanProperty(False)
font_name = StringProperty(DEFAULT_FONT) font_name = StringProperty(DEFAULT_FONT)
disabled = BooleanProperty(False) disabled = BooleanProperty(False)
tri_state = BooleanProperty(False)
def trigger_action(self, *args, **kwargs): def trigger_action(self, *args, **kwargs):
return self.checkbox.trigger_action(*args, **kwargs) return self.checkbox._do_press()
def activate(self, *_args): def activate(self, *_args):
self.checkbox.active = True self.checkbox.active = True
@@ -559,6 +586,10 @@ class ClickableLabel(LeftButtonBehavior, Label):
pass pass
class ClickableCircle(LeftButtonBehavior, CircleWithText):
pass
class ScrollableLabel(ScrollView, BackgroundMixin): class ScrollableLabel(ScrollView, BackgroundMixin):
__events__ = ["on_ref_press"] __events__ = ["on_ref_press"]
outline_color = ListProperty([0, 0, 0, 0]) # mixin not working for some reason outline_color = ListProperty([0, 0, 0, 0]) # mixin not working for some reason
+25 -10
View File
@@ -238,6 +238,7 @@ class ConfigTimerPopup(QuickConfigGui):
p.periods_used = 0 p.periods_used = 0
self.katrain.controls.timer.paused = True self.katrain.controls.timer.paused = True
self.katrain.game.current_node.time_used = 0 self.katrain.game.current_node.time_used = 0
self.katrain.game.main_time_used = 0
self.katrain.update_state() self.katrain.update_state()
@@ -245,7 +246,7 @@ class NewGamePopup(QuickConfigGui):
def __init__(self, katrain): def __init__(self, katrain):
super().__init__(katrain) super().__init__(katrain)
for bw, info in katrain.players_info.items(): 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] 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.update_player(bw, **player_setup.player_type_dump)
self.katrain("new-game") 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): def wrap_anchor(widget):
anchor = AnchorLayout() anchor = AnchorLayout()
@@ -341,7 +356,7 @@ class ConfigAIPopup(QuickConfigGui):
self.options_grid.clear_widgets() self.options_grid.clear_widgets()
self.help_label.text = i18n._(strategy.replace("ai:", "aihelp:")) 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])): 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: if k in AI_OPTION_VALUES:
values = AI_OPTION_VALUES[k] values = AI_OPTION_VALUES[k]
if values == "bool": if values == "bool":
@@ -482,20 +497,20 @@ class ConfigPopup(QuickConfigGui):
KATAGOS = { KATAGOS = {
"win": { "win": {
"OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-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.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-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.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-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", "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": { "linux": {
"OpenCL v1.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-gpu-opencl-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.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-avx2-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.6.1": "https://github.com/lightvector/KataGo/releases/download/v1.6.1/katago-v1.6.1-cpu-eigen-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", "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": { "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.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.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 (Linux)": "https://github.com/lightvector/KataGo/releases/download/v1.7.0/katago-v1.7.0-gpu-cuda10.2-linux-x64.zip",
}, },
} }
+29 -8
View File
@@ -3,7 +3,7 @@ from collections import defaultdict
from kivy.graphics.context_instructions import Color from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Line, Rectangle from kivy.graphics.vertex_instructions import Line, Rectangle
from kivy.lang import Builder 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.dropdown import DropDown
from kivy.uix.scrollview import ScrollView from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget 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.kivyutils import BackgroundMixin, draw_circle, draw_text
from katrain.gui.style import ( from katrain.gui.style import (
WHITE,
STONE_COLORS,
LIGHTGREY,
BACKGROUND_COLOR, BACKGROUND_COLOR,
OUTLINE_COLORS,
LIGHTER_BACKGROUND_COLOR, LIGHTER_BACKGROUND_COLOR,
YELLOW, LIGHTGREY,
STONE_TEXT_COLORS, OUTLINE_COLORS,
RED, RED,
STONE_COLORS,
STONE_TEXT_COLORS,
WHITE,
YELLOW,
) )
@@ -85,6 +85,17 @@ class MoveTreeCanvas(Widget):
self.set_game_node(parent) self.set_game_node(parent)
self.is_open = False 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): def switch_branch(self, direction=1):
pos = self.move_pos.get(self.scroll_view_widget.current_node) pos = self.move_pos.get(self.scroll_view_widget.current_node)
if not self.scroll_view_widget: if not self.scroll_view_widget:
@@ -96,7 +107,7 @@ class MoveTreeCanvas(Widget):
self.set_game_node(same_x_moves[new_index][1]) self.set_game_node(same_x_moves[new_index][1])
def draw_move_tree(self, current_node): def draw_move_tree(self, current_node):
if not self.scroll_view_widget: if not self.scroll_view_widget or not current_node:
return return
spacing = 5 spacing = 5
@@ -109,6 +120,7 @@ class MoveTreeCanvas(Widget):
stack = root.ordered_children[::-1] stack = root.ordered_children[::-1]
next_y_pos = defaultdict(int) # x pos -> max y pos 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 = defaultdict(list) # since AI self-play etc may modify the tree between layout and draw!
children[root] = root.ordered_children
while stack: while stack:
move = stack.pop() move = stack.pop()
x = move.depth x = move.depth
@@ -176,6 +188,9 @@ class MoveTree(ScrollView, BackgroundMixin):
def delete_selected_node(self): def delete_selected_node(self):
self.move_tree_canvas.delete_selected_node() 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): def scroll_to_pixel(self, x, y):
if not self._viewport: if not self._viewport:
return return
@@ -219,5 +234,11 @@ Builder.load_string(
shortcut: '' shortcut: ''
on_action: root.katrain.controls.move_tree.delete_selected_node() on_action: root.katrain.controls.move_tree.delete_selected_node()
-background_color: LIGHTER_BACKGROUND_COLOR -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
""" """
) )
+1 -1
View File
@@ -127,7 +127,7 @@ KV = """
Label: Label:
id: label id: label
text: slider.values[slider.index][1] text: slider.values[slider.index][1]
font_size: dp(12) font_size: sp(12)
lang_change_tracking: i18n._('') # for font lang_change_tracking: i18n._('') # for font
halign: "center" halign: "center"
color: root.thumb_color if root.active else [0, 0, 0, 0] color: root.thumb_color if root.active else [0, 0, 0, 0]
Binary file not shown.
+51 -1
View File
@@ -521,7 +521,6 @@ msgstr "读秒声音开启"
msgid "tab:info-details" msgid "tab:info-details"
msgstr "详细" msgstr "详细"
#. TODO: updated
msgid "teaching undo message" msgid "teaching undo message"
msgstr "撤销第 {move} 手棋因为损失超过 {points_lost:.1f} 目. 悬停在该手棋 上方查看信息. 请再次尝试." msgstr "撤销第 {move} 手棋因为损失超过 {points_lost:.1f} 目. 悬停在该手棋 上方查看信息. 请再次尝试."
@@ -640,3 +639,54 @@ msgstr "寻找其他走法"
#. status message #. status message
msgid "alternative analysis" msgid "alternative analysis"
msgstr "正在搜索其他走法" 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"
Binary file not shown.
+50 -4
View File
@@ -671,12 +671,12 @@ msgstr ""
msgid "show point loss as text" msgid "show point loss as text"
msgstr "Zeig den Punkteverlust auf Beste Züge" msgstr "Zeig den Punkteverlust auf Beste Züge"
#. TODO - updated
msgid "teaching undo message" msgid "teaching undo message"
msgstr "" msgstr ""
"Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bewege " "Zug {move} zurückgenommen, da er {points_lost:.1f} Punkte verliert. Bitte "
"Mauszeiger über den Zug,um die erwartete Widerlegung zu sehen. Bitte " "versuche es erneut. Bewege den Mauszeiger über den Zug, um die erwartete "
"versuche es erneut." "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 #. resign the game
msgid "resign" msgid "resign"
@@ -689,3 +689,49 @@ msgstr "Finde alternative Züge"
#. - status message #. - status message
msgid "alternative analysis" msgid "alternative analysis"
msgstr "Suche nach alternativen Zügen" 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"
Binary file not shown.
+63 -2
View File
@@ -106,6 +106,10 @@ msgstr "Deeper full game analysis"
msgid "analysis:alternatives" msgid "analysis:alternatives"
msgstr "Find alternative moves" 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 #. Keep improving current position analysis when there is nothing else to do
msgid "analysis:continuous" msgid "analysis:continuous"
msgstr "Toggle continuous analysis" msgstr "Toggle continuous analysis"
@@ -125,6 +129,10 @@ msgstr "Analyzing entire board using {visits} visits/move"
msgid "alternative analysis" msgid "alternative analysis"
msgstr "Searching for alternative moves" msgstr "Searching for alternative moves"
#. status message
msgid "local analysis"
msgstr "Searching for local responses"
#. status message #. status message
msgid "equalizing analysis" msgid "equalizing analysis"
msgstr "Equalizing analysis of candidate moves to {visits} visits" msgstr "Equalizing analysis of candidate moves to {visits} visits"
@@ -293,14 +301,25 @@ msgid "timer settings"
msgstr "Adjust Timer Settings" msgstr "Adjust Timer Settings"
msgid "byoyomi length" msgid "byoyomi length"
msgstr "Time per Byo-yomi Period" msgstr "Time per byo-yomi period (s)"
msgid "byoyomi periods" msgid "byoyomi periods"
msgstr "Number of Periods" msgstr "Number of periods"
msgid "count down sound" msgid "count down sound"
msgstr "Sound on" 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 #. submit button
msgid "update timer" msgid "update timer"
msgstr "Update Timer" msgstr "Update Timer"
@@ -351,6 +370,12 @@ msgstr "Use x:y (e.g. 19:9) for non-square size"
msgid "new game" msgid "new game"
msgstr "Start 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 # teacher settings from here
msgid "teacher settings" msgid "teacher settings"
msgstr "Edit Teacher Settings" msgstr "Edit Teacher Settings"
@@ -560,6 +585,38 @@ msgstr ""
"perspective. When `automatic` is set, KaTrain will find a suitable value " "perspective. When `automatic` is set, KaTrain will find a suitable value "
"automatically." "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" msgid "ai:jigo"
msgstr "KataJigo" msgstr "KataJigo"
@@ -691,6 +748,10 @@ msgstr "Number of visits per move"
msgid "Delete Node" msgid "Delete Node"
msgstr "Delete Node" msgstr "Delete Node"
#. on move tree editing
msgid "Make Main Branch"
msgstr "Make Main Branch"
#. theme label #. theme label
msgid "theme" msgid "theme"
msgstr "Colour Theme" msgstr "Colour Theme"
Binary file not shown.
@@ -697,3 +697,58 @@ msgstr "Find alternative moves"
#. TODO - status message #. TODO - status message
msgid "alternative analysis" msgid "alternative analysis"
msgstr "Searching for alternative moves" 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"
Binary file not shown.
+60 -5
View File
@@ -93,7 +93,7 @@ msgid "analysis:extra"
msgstr "Analyser davantage" msgstr "Analyser davantage"
msgid "analysis:equalize" msgid "analysis:equalize"
msgstr "Égaliser l’analyse des coups" msgstr "Égaliser l'analyse des coups"
msgid "analysis:sweep" msgid "analysis:sweep"
msgstr "Analyser tous les possibles" msgstr "Analyser tous les possibles"
@@ -105,7 +105,7 @@ msgid "analysis:continuous"
msgstr "Lancer/Arrêter l'analyse continue" msgstr "Lancer/Arrêter l'analyse continue"
msgid "analysis:aimove" 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 #. label in little popup for analyzing entire game
msgid "reanalyze max visits" msgid "reanalyze max visits"
@@ -525,9 +525,10 @@ msgstr ""
#. TODO: updated #. TODO: updated
msgid "teaching undo message" msgid "teaching undo message"
msgstr "" msgstr ""
"Le coup {move} a été annulé car il perdait {points_lost:.1f} points. Vous " "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 " "pouvez consulter les Prévisions territoriales (t) pour identifier les "
"coup." "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" msgid "sweep analysis"
msgstr "Analyse de tous les possibles ({visits} visites/coup)." msgstr "Analyse de tous les possibles ({visits} visites/coup)."
@@ -721,3 +722,57 @@ msgstr "Proposer des coups différents"
msgid "alternative analysis" msgid "alternative analysis"
msgstr "Recherche de coups différents." 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"
Binary file not shown.
@@ -679,3 +679,55 @@ msgstr ""
#. status message #. status message
msgid "alternative analysis" msgid "alternative analysis"
msgstr "他の候補手を分析中" 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 "こちらを主分岐に"
Binary file not shown.
+55 -5
View File
@@ -546,11 +546,6 @@ msgstr ""
"`threshold`선 밑의 수를 더 많이 뽑은 후 그 중 최선의 수를 둡니다. `line_weight`을 증가시키면 바둑판 중앙에 있는" "`threshold`선 밑의 수를 더 많이 뽑은 후 그 중 최선의 수를 둡니다. `line_weight`을 증가시키면 바둑판 중앙에 있는"
" 수를 더욱 꺼리게 됩니다. 판이 `endgame` 비율만큼 차면 평범하게 둡니다." " 수를 더욱 꺼리게 됩니다. 판이 `endgame` 비율만큼 차면 평범하게 둡니다."
#. TODO: updated
msgid "teaching undo message"
msgstr ""
"{points_lost:.1f}집 손해이므로 {move}을/를 물렀습니다. 커서를 올려 상대의 대응책을 보십시오. 다시 두어 주세요."
msgid "aihelp:p:rank" msgid "aihelp:p:rank"
msgstr "" msgstr ""
"제한적인 착수 가능 목록에서 무작위로 다음 착수를 선택한 후 그 중 최선의 수를 둡니다. 더 강한 설정은 최선의 수를 더 큰 목록에서 " "제한적인 착수 가능 목록에서 무작위로 다음 착수를 선택한 후 그 중 최선의 수를 둡니다. 더 강한 설정은 최선의 수를 더 큰 목록에서 "
@@ -654,3 +649,58 @@ msgstr "다른 수 찾기"
msgid "alternative analysis" msgid "alternative analysis"
msgstr "다른 수를 찾는 중입니다" 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 "주 변화로 만들기"
Binary file not shown.
+55 -1
View File
@@ -407,7 +407,7 @@ msgid "engine:max_time"
msgstr "Максимальное время анализа" msgstr "Максимальное время анализа"
msgid "engine:wide_root_noise" msgid "engine:wide_root_noise"
msgstr "Высокий уровень шума (увеличивает разнообразие рассматриваемых ходов)" msgstr "Уровень шума (увеличивает разнообразие рассматриваемых ходов)"
msgid "engine:wide_root_noise:hint" msgid "engine:wide_root_noise:hint"
msgstr "" msgstr ""
@@ -684,3 +684,57 @@ msgstr "Поискать альтернативные ходы"
msgid "alternative analysis" msgid "alternative analysis"
msgstr "Поиск альтернативных ходов" 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 "Сделать главной веткой"
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+38 -17
View File
@@ -8,8 +8,8 @@
#:set CP_SMALL_SPACING 3 #:set CP_SMALL_SPACING 3
#:set CP_PADDING 6 #:set CP_PADDING 6
#:set INPUT_FONT_SIZE dp(25) #:set INPUT_FONT_SIZE sp(25)
#:set DESC_FONT_SIZE dp(20) #:set DESC_FONT_SIZE sp(20)
#:set LIGHTGREY [0.7,0.7,0.7,1] #:set LIGHTGREY [0.7,0.7,0.7,1]
#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR #:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR
@@ -32,7 +32,7 @@
line_color_focus: TEXT_COLOR line_color_focus: TEXT_COLOR
<LabelledPathInput>: <LabelledPathInput>:
font_size: dp(16) font_size: sp(16)
multiline: False multiline: False
helper_text_mode: "on_error" helper_text_mode: "on_error"
helper_text: i18n._("file not found") helper_text: i18n._("file not found")
@@ -179,13 +179,18 @@
orientation: 'horizontal' orientation: 'horizontal'
GridLayout: GridLayout:
cols: 2 cols: 2
rows: 4 rows: 5
spacing: CP_SPACING spacing: CP_SPACING
DescriptionLabel: DescriptionLabel:
text: i18n._("general:sgf_save") text: i18n._("general:sgf_save")
AnchorLayout: AnchorLayout:
LabelledPathInput: LabelledPathInput:
input_property: "general/sgf_save" input_property: "general/sgf_save"
DescriptionLabel:
text: i18n._('count down sound')
AnchorLayout:
LabelledCheckBox:
input_property: "timer/sound"
DescriptionLabel: DescriptionLabel:
text: i18n._("general:anim_pv_time") text: i18n._("general:anim_pv_time")
AnchorLayout: AnchorLayout:
@@ -250,7 +255,7 @@
AutoSizedRoundedRectangleButton: AutoSizedRoundedRectangleButton:
padding_x: 15 padding_x: 15
size_hint: None,None size_hint: None,None
height: dp(40) height: sp(40)
text: i18n._("update settings") text: i18n._("update settings")
on_press: root.update_config(True) on_press: root.update_config(True)
@@ -319,9 +324,15 @@
<ConfigTimerPopup>: <ConfigTimerPopup>:
MDGridLayout: MDGridLayout:
cols: 2 cols: 2
rows: 3 rows: 4
spacing: 1.5 * CP_SPACING spacing: 1.5 * CP_SPACING
size_hint: 1,2.5 size_hint: 1,2.5
DescriptionLabel:
text: i18n._('main time')
AnchorLayout:
LabelledIntInput:
text: "10"
input_property: "timer/main_time"
DescriptionLabel: DescriptionLabel:
text: i18n._('byoyomi length') text: i18n._('byoyomi length')
AnchorLayout: AnchorLayout:
@@ -335,10 +346,11 @@
text: "5" text: "5"
input_property: "timer/byo_periods" input_property: "timer/byo_periods"
DescriptionLabel: DescriptionLabel:
text: i18n._('count down sound') text: i18n._('minimal time use')
AnchorLayout: AnchorLayout:
LabelledCheckBox: LabelledIntInput:
input_property: "timer/sound" text: "0"
input_property: "timer/minimal_use"
AnchorLayout: AnchorLayout:
size_hint: 1,1 size_hint: 1,1
AutoSizedRoundedRectangleButton: AutoSizedRoundedRectangleButton:
@@ -443,13 +455,22 @@
input_property: 'game/clear_cache' input_property: 'game/clear_cache'
SmallDescriptionLabel: SmallDescriptionLabel:
text: i18n._('avoids replaying') text: i18n._('avoids replaying')
AnchorLayout: BoxLayout:
size_hint: 1,1 size_hint: 1,0.75
AutoSizedRoundedRectangleButton: AnchorLayout:
padding_x: 15 size_hint: 0.5, 1
size_hint: None,0.5 AutoSizedRoundedRectangleButton:
text: i18n._("new game") padding_x: 15
on_press: root.update_config(True) 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)
<LabelledSelectionSlider>: <LabelledSelectionSlider>:
@@ -457,7 +478,7 @@
textbox: textbox textbox: textbox
SelectionSlider: SelectionSlider:
id: slider id: slider
size_hint: 2,1 size_hint: 2.25,1
values: root.values values: root.values
on_change: textbox.text = str(slider.value) on_change: textbox.text = str(slider.value)
track_color: LIGHTGREY track_color: LIGHTGREY
Binary file not shown.
+5 -2
View File
@@ -41,16 +41,19 @@ setup(
install_requires=[ install_requires=[
"wheel", "wheel",
"setuptools", "setuptools",
"urllib3",
"importlib_resources ;python_version<'3.7'", "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 "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.glew;platform_system=='Windows'",
"kivy_deps.sdl2;platform_system=='Windows'", "kivy_deps.sdl2;platform_system=='Windows'",
"kivy_deps.gstreamer;platform_system=='Windows'", "kivy_deps.gstreamer;platform_system=='Windows'",
"kivy==2.0.0rc2", # rc3 failing on mac
"kivymd>=0.104.1", "kivymd>=0.104.1",
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos "screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
], ],
dependency_links=["https://kivy.org/downloads/simple/"],
python_requires=">=3.6, <4", python_requires=">=3.6, <4",
entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]}, entry_points={"console_scripts": ["katrain=katrain.__main__:run_app"]},
classifiers=[ classifiers=[