Merge pull request #1 from sanderland/master

Merge upstream changes
This commit is contained in:
pdeblanc authored and GitHub committed 2020-12-03 18:42:16 +09:00
commit ca9fa5b7ae
47 files changed
+968 -178

No files matched your search

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

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

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