47 files changed
+968
-178
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -16,6 +16,8 @@ tmp.pickle
|
||||
my
|
||||
logs
|
||||
callgrind.*
|
||||
cpp/out
|
||||
.vs
|
||||
|
||||
# debug
|
||||
outdated_log.txt
|
||||
|
||||
+2
-1
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
Binary file not shown.
+45
-22
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
)
|
||||
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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
@@ -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.
@@ -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=[
|
||||
|
||||
Reference in new issue
Block a user