This commit is contained in:
Sander Land committed 2020-05-25 17:12:58 +02:00
1 parent a646c32939
commit 40ae451bef
12 files changed
+281 -324

No files matched your search

+11 -12
View File
@@ -4,7 +4,7 @@ from collections import defaultdict
localedir = "katrain/i18n/locales"
locales = set(os.listdir(localedir))
print("locales found:",locales)
print("locales found:", locales)
strings_to_langs = defaultdict(dict)
lang_to_strings = defaultdict(set)
@@ -14,11 +14,11 @@ DEFAULT_LANG = "en"
po = {}
pofile = {}
for lang in locales:
pofile[lang] = os.path.join(localedir, lang, 'LC_MESSAGES', 'katrain.po')
pofile[lang] = os.path.join(localedir, lang, "LC_MESSAGES", "katrain.po")
po[lang] = polib.pofile(pofile[lang])
for entry in po[lang].translated_entries():
if 'TODO' in entry.comment:
print(lang,'/',entry.msgid,'is TODO')
if "TODO" in entry.comment:
print(lang, "/", entry.msgid, "is TODO")
else:
strings_to_langs[entry.msgid][lang] = entry.msgstr
lang_to_strings[lang].add(entry.msgid)
@@ -26,16 +26,15 @@ for lang in locales:
for lang in locales:
for msgid in strings_to_langs.keys() - lang_to_strings[lang]:
if lang==DEFAULT_LANG:
print("Message id",msgid,"found as ",strings_to_langs[msgid],"but missing in default",DEFAULT_LANG)
if lang == DEFAULT_LANG:
print("Message id", msgid, "found as ", strings_to_langs[msgid], "but missing in default", DEFAULT_LANG)
elif DEFAULT_LANG in strings_to_langs[msgid]:
print("Message id", msgid, "missing in ",lang,'-> Adding it from',DEFAULT_LANG)
entry = polib.POEntry(msgid=msgid,msgstr=strings_to_langs[msgid][DEFAULT_LANG],comment="TODO")
print("Message id", msgid, "missing in ", lang, "-> Adding it from", DEFAULT_LANG)
entry = polib.POEntry(msgid=msgid, msgstr=strings_to_langs[msgid][DEFAULT_LANG], comment="TODO")
po[lang].append(entry)
else:
print(f"MISSING IN DEFAULT AND {lang}",strings_to_langs[msgid])
print(f"MISSING IN DEFAULT AND {lang}", strings_to_langs[msgid])
po[lang].save(pofile[lang])
mofile = pofile[lang].replace('.po', '.mo')
mofile = pofile[lang].replace(".po", ".mo")
po[lang].save_as_mofile(mofile)
print('Fixed',pofile[lang],'and converted ->',mofile)
print("Fixed", pofile[lang], "and converted ->", mofile)
+29 -23
View File
@@ -1,38 +1,46 @@
from kivy.config import Config # isort:skip
from kivy.properties import ObjectProperty
ICON = "img/icon.png"
Config.set("kivy", "window_icon", ICON) # isort:skip # set icon
Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click
import signal
import os
import signal
import sys
import threading
import traceback
from queue import Queue
from kivy.lang import Builder
from kivy.resources import resource_add_path
from kivy.app import App
from kivymd.app import MDApp
from kivy.core.clipboard import Clipboard
from kivy.lang import Builder
from kivy.properties import ObjectProperty
from kivy.resources import resource_add_path
from kivy.storage.jsonstore import JsonStore
from kivy.uix.popup import Popup
from kivy.uix.screenmanager import Screen
from kivymd.app import MDApp
from katrain.core.ai import ai_move
from katrain.core.common import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_DEBUG, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR, find_package_resource, LANGUAGE
from katrain.core.common import (
LANGUAGE,
OUTPUT_DEBUG,
OUTPUT_ERROR,
OUTPUT_EXTRA_DEBUG,
OUTPUT_INFO,
OUTPUT_KATAGO_STDERR,
Lang,
find_package_resource,
)
from katrain.core.engine import KataGoEngine
from katrain.core.game import Game, IllegalMoveException, KaTrainSGF
from katrain.core.sgf_parser import Move, ParseError
from katrain.gui.kivyutils import *
from katrain.gui.graph import ScoreGraph
from katrain.gui.badukpan import BadukPanWidget, AnalysisControls, BadukPanControls
from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget
from katrain.gui.controlspanel import ControlsPanel, RightButtonControls
from katrain.gui.popups import NewGamePopup, ConfigPopup, LoadSGFPopup
from katrain.core.common import Lang
from katrain.gui.graph import ScoreGraph
from katrain.gui.kivyutils import *
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup
from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL
ICON = "img/icon.png"
Config.set("kivy", "window_icon", ICON) # isort:skip # set icon
Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click
__version__ = "1.1.0"
@@ -105,7 +113,6 @@ class KaTrainGui(Screen):
if self.engine:
return
self.board_gui.trainer_config = self.config("trainer")
self.board_gui.ui_config = self.config("board_ui")
self.engine = KataGoEngine(self, self.config("engine"))
threading.Thread(target=self._message_loop_thread, daemon=True).start()
self._do_new_game()
@@ -139,11 +146,11 @@ class KaTrainGui(Screen):
# update engine status dot
if not self.engine or not self.engine.katago_process or self.engine.katago_process.poll() is not None:
self.controls.engine_status_col = self.config("board_ui/engine_down_col")
self.controls.engine_status_col = ENGINE_DOWN_COL
elif len(self.engine.queries) == 0:
self.controls.engine_status_col = self.config("board_ui/engine_ready_col")
self.controls.engine_status_col = ENGINE_READY_COL
else:
self.controls.engine_status_col = self.config("board_ui/engine_busy_col")
self.controls.engine_status_col = ENGINE_BUSY_COL
# redraw
if redraw_board:
@@ -203,7 +210,7 @@ class KaTrainGui(Screen):
def _do_play(self, coords):
self.board_gui.animating_pv = None
try:
self.game.play(Move(coords, player=self.game.next_player))
self.game.play(Move(coords, player=self.game.next_player.player))
except IllegalMoveException as e:
self.controls.set_status(f"Illegal Move: {str(e)}")
@@ -331,7 +338,6 @@ class KaTrainGui(Screen):
return True
class KaTrainApp(MDApp):
gui = ObjectProperty(None)
language = StringProperty(LANGUAGE)
+2 -117
View File
@@ -51,7 +51,8 @@
],
"eval_off_show_last": 3,
"eval_show_ai": true,
"lock_ai": false
"lock_ai": false,
"anim_pv_time": 0.5
},
"ai": {
"Default": {
@@ -133,122 +134,6 @@
"_help_right": "Increase `line_weight` to penalize moves closer to the center more. Stops strategy after the 'endgame' fraction of the board is filled."
}
},
"board_ui": {
"anim_pv_time": 0.5,
"engine_down_col": [
0.8,
0,
0,
1
],
"engine_busy_col": [
0.9,
0.4,
0.1,
1
],
"engine_little_busy_col": [
0.85,
0.89,
0.3,
1
],
"engine_almost_done_col": [
0.67,
0.9,
0.18,
1
],
"engine_ready_col": [
0.117,
0.588,
0,
1
],
"board_color": [0.85, 0.68, 0.40,1],
"starpoint_size": 0.1,
"stone_size": 0.475,
"eval_dot_max_size": 0.5,
"eval_dot_min_size": 0.25,
"visit_frac_small": 0.1,
"stones": {
"B": [
0.05,
0.05,
0.05
],
"W": [
0.95,
0.95,
0.95
]
},
"outline": {
"B": [
0.3,
0.3,
0.3,
0.5
],
"W": [
0.7,
0.7,
0.7,
0.5
]
},
"ghost_alpha": 0.5,
"top_move_x_alpha": 0.3,
"child_scale": 0.95,
"eval_colors": [
[
0.447,
0.129,
0.42,
1
],
[
0.8,
0,
0,
1
],
[
0.9,
0.4,
0.1,
1
],
[
0.85,
0.89,
0.3,
1
],
[
0.67,
0.9,
0.18,
1.0
],
[
0.117,
0.588,
0,
1.0
]
],
"line_color": [
0,
0,
0
],
"policy_color": [
0.9,
0.2,
0.8
]
},
"debug": {
"level": 1,
"_hint_level": "0: Little console output.\n1: Some extra output\nfor error reporting."
+9 -6
View File
@@ -17,9 +17,7 @@ OUTPUT_INFO = 0
OUTPUT_DEBUG = 1
OUTPUT_EXTRA_DEBUG = 2
PLAYER_HUMAN = 0
PLAYER_HUMAN_TEACHING = 1
PLAYER_AI = 2
MODE_PLAY, MODE_ANALYZE = "play", "analyze"
def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]:
@@ -63,7 +61,8 @@ class Lang(Observable):
self.switch_lang(self.lang)
def _(self, text):
return self.ugettext(text)
str = self.ugettext(text)
return str
def fbind(self, name, func, *args, **kwargs):
if name == "_":
@@ -74,7 +73,7 @@ class Lang(Observable):
def funbind(self, name, func, *args, **kwargs):
if name == "_":
key = (func, args, kwargs)
print('funbind',key in self.observers)
print("funbind", key in self.observers)
if key in self.observers:
self.observers.remove(key)
else:
@@ -92,16 +91,20 @@ class Lang(Observable):
try:
func(args[0], None, None)
except ReferenceError:
pass # proxy no longer exists
pass # proxy no longer exists
LANGUAGE = "nl"
i18n = Lang(LANGUAGE)
class I18NLabel(Label):
pass
class I18NCoreLabel(CoreLabel):
pass
class I18NTextInput(TextInput):
pass
+43 -5
View File
@@ -19,15 +19,47 @@ class KaTrainSGF(SGF):
_NODE_CLASS = GameNode
PLAYER_HUMAN, PLAYER_AI = "Player", "AI"
PLAYING_NORMAL, PLAYING_TEACHING = "Normal game", "Teaching game"
AI_DEFAULT = "default"
class Player:
def __init__(self, player="B", player_type=PLAYER_HUMAN, player_subtype=PLAYING_NORMAL, periods_used=0):
self.player = player
self.player_type = player_type
self.player_subtype = player_subtype
self.periods_used = periods_used
@property
def ai(self):
return self.player_type == PLAYER_AI
@property
def human(self):
return self.player_type == PLAYER_HUMAN
@property
def being_taught(self):
return self.player_type == PLAYER_HUMAN and self.player_subtype == PLAYING_TEACHING
def strategy(self):
return self.player_subtype if self.ai else AI_DEFAULT
def __str__(self):
return f"{self.player_type} ({self.player_subtype})"
class Game:
"""Represents a game of go, including an implementation of capture rules."""
DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "RU": "JP", "AP": "KaTrain:https://github.com/sanderland/katrain"}
def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, move_tree: GameNode = None, analyze_fast=False):
def __init__(self, katrain, engine: Union[Dict, KataGoEngine], config: Dict, move_tree: GameNode = None, players: Dict = None, analyze_fast=False):
self.katrain = katrain
if not isinstance(engine, Dict):
engine = {"B": engine, "W": engine}
self.players = players or {"B": Player("B"), "W": Player("W")}
self.engines = engine
self.config = config
self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S")
@@ -42,10 +74,20 @@ class Game:
board_size = config.get("init_size", 19)
self.komi = self.config.get("init_komi", 6.5)
self.root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **{"SZ": board_size, "KM": self.komi, "DT": self.game_id}})
self.root.set_property("PW", str(self.players["W"]))
self.root.set_property("PB", str(self.players["B"]))
self.set_current_node(self.root)
threading.Thread(target=lambda: self.analyze_all_nodes(-1_000_000, analyze_fast=analyze_fast), daemon=True).start() # return faster, but bypass Kivy Clock
@property
def last_player(self) -> Player:
return self.players[self.current_node.player]
@property
def next_player(self) -> Player:
return self.players[self.current_node.next_player]
def analyze_all_nodes(self, priority=0, analyze_fast=False):
for node in self.root.nodes_in_tree:
node.analyze(self.engines[node.next_player], priority=priority, analyze_fast=analyze_fast)
@@ -181,10 +223,6 @@ class Game:
def board_size(self):
return self.root.board_size
@property
def next_player(self):
return self.current_node.next_player
@property
def stones(self):
return sum(self.chains, [])
-2
View File
@@ -94,7 +94,6 @@ class GameNode(SGFNode):
def analysis_ready(self):
return self.analysis["root"] is not None
@property
def score(self) -> Optional[float]:
if self.analysis_ready:
@@ -179,7 +178,6 @@ class GameNode(SGFNode):
score = self.score
return self.player_sign(single_move.player) * (score - parent_parent_score)
@staticmethod
def player_sign(player):
return {"B": 1, "W": -1, None: 0}[player]
+27 -32
View File
@@ -30,9 +30,6 @@
<I18NLabel>:
font_name: i18n._('Roboto')
<I18NCoreLabel>:
font_name: i18n._('Roboto')
<I18NTextInput>:
font_name: i18n._('Roboto')
@@ -100,11 +97,11 @@
<AutoSizedMDFlatRectangleButton>: # only wants height
width: self.label.texture_size[0] + self.hor_padding * 2
font_size: 0.75 * self.height / len(self.label.text.split('\n'))
font_size: 0.8 * self.height / len(self.label.text.split('\n'))
<AutoSizedMDFlatRectangleToggleButton>: # only wants height
width: self.label.texture_size[0] + self.hor_padding * 2
font_size: 0.75 * self.height / len(self.label.text.split('\n'))
font_size: 0.8 * self.height / len(self.label.text.split('\n'))
<BGMDBoxLayout@MDBoxLayout+BackgroundColor>:
background_color: BOX_BACKGROUND_COLOR
@@ -253,19 +250,19 @@
<CircleWithText>:
min_size: min(self.height,self.width)
Image:
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
id: image
pos: (root.pos[0] + (root.width - root.min_size)/2,root.pos[1] + (root.height - root.min_size)/2)
size: (root.min_size,root.min_size)
source: 'img/white.png' if root.player=='White' else 'img/black.png'
mipmap: True
I18NLabel:
Label:
id: lbl
text: root.text
bold: True
color: BLACK if root.player=='White' else WHITE
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
size: (root.min_size,root.min_size)
halign: 'center'
valign: 'middle'
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text)) # TODO: easier?
pos: (image.x, image.y+2)
size: image.size
font_size: root.min_size * 1.2 / (1 + 1 * len(self.text))
# UI components
@@ -387,8 +384,6 @@
# ---------- components of right pane -------
<PlayerInfo@MDBoxLayout>:
captures: 0
name: 'Player'
@@ -548,26 +543,24 @@
opacity: int(root.show_winrate)
<StatsBox@BoxLayout+BackgroundColor>
<StatsBox>
orientation: 'vertical'
background_color: BOX_BACKGROUND_COLOR
score: score
winrate: winrate
score_change: score_change
labels: {'score':score_label,'winrate':winrate_label,'points':pointloss_label}
CensorableLabel:
id: winrate
id: winrate_label
label: 'Win rate'
text: ''
text: root.winrate
color: WINRATE_COLOR
CensorableLabel:
id: score
id: score_label
label: 'Score'
text: ''
text: root.score
color: SCORE_COLOR
CensorableLabel:
id: score_change
label: 'Points Lost'
text: ''
id: pointloss_label
label: 'Points Lost' if root.points_lost is None or root.points_lost > 0 else 'Points gained'
text: '{}: {:.1f}'.format(root.player,abs(root.points_lost)) if root.points_lost is not None else '...'
color: POINTLOSS_COLOR
<ScrollableLabel>:
@@ -678,7 +671,7 @@
on_ref_press: root.katrain.board_gui.show_pv_from_comments(args[1])
size_hint: 1, None
height: self.parent.height - status.height - note.height - 1
TextInput:
I18NTextInput:
id: note
size_hint: 1, None
on_text: root.katrain.set_note(self.text)
@@ -765,6 +758,11 @@
font_size: root.font_size
color: GREY
<LangButton@SizedMDFlatRectangleButton>
size_hint: 1,1
font_size: 0.4 * self.height
<ContentNavigationDrawer@MDBoxLayout+BackgroundColor>:
katrain: app.gui
background_color: BOX_BACKGROUND_COLOR
@@ -783,18 +781,15 @@
Line:
rectangle: (*self.pos,*self.size)
width: 1
SizedMDFlatButton:
LangButton:
text: i18n._('English')
on_press: app.language = 'en'
size_hint: 1/3,1
SizedMDFlatButton:
LangButton:
text: i18n._('Dutch')
on_press: app.language = 'nl'
size_hint: 1/3,1
SizedMDFlatButton:
LangButton:
text: i18n._('Korean')
on_press: app.language = 'ko'
size_hint: 1/3,1
Label:
MDBoxLayout:
canvas.before:
+49 -40
View File
@@ -11,17 +11,33 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.menu import MDDropdownMenu
from katrain.core.common import OUTPUT_DEBUG, evaluation_class, PLAYER_AI, i18n
from katrain.core.common import OUTPUT_DEBUG, evaluation_class, i18n, MODE_PLAY
from katrain.core.game import Move
from katrain.gui.kivyutils import draw_circle, draw_text
from katrain.core.common import var_to_grid
from kivy.core.window import Window
from katrain.gui.style import (
EVAL_COLORS,
LINE_COLOR,
POLICY_COLOR,
GHOST_ALPHA,
TOP_MOVE_ALPHA,
STONE_COLORS,
OUTLINE_COLORS,
CHILD_SCALE,
EVAL_DOT_MIN_SIZE,
EVAL_DOT_MAX_SIZE,
BOARD_COLOR,
STARPOINT_SIZE,
STONE_SIZE,
VISITS_FRAC_SMALL,
)
class BadukPanWidget(Widget):
def __init__(self, **kwargs):
super(BadukPanWidget, self).__init__(**kwargs)
self.ui_config = {}
self.trainer_config = {}
self.ghost_stone = []
self.gridpos_x = []
@@ -124,7 +140,7 @@ class BadukPanWidget(Widget):
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=min(2, 0.035 * stone_size))
if evalcol:
eval_radius = math.sqrt(evalscale) # scale area by evalscale
evalsize = self.stone_size * (self.ui_config["eval_dot_min_size"] + eval_radius * (self.ui_config["eval_dot_max_size"] - self.ui_config["eval_dot_min_size"]))
evalsize = self.stone_size * (EVAL_DOT_MIN_SIZE + eval_radius * (EVAL_DOT_MAX_SIZE - EVAL_DOT_MIN_SIZE))
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), evalsize, evalcol)
if innercol:
@@ -132,12 +148,12 @@ class BadukPanWidget(Widget):
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size * 0.475 / 0.85), width=0.1 * stone_size)
def eval_color(self, points_lost):
colors = self.ui_config["eval_colors"]
colors = EVAL_COLORS
i = evaluation_class(points_lost, self.trainer_config["eval_thresholds"])
return colors[i]
def draw_board(self, *_args):
if not self.ui_config:
if not (self.katrain and self.katrain.game):
return
katrain = self.katrain
board_size_x, board_size_y = katrain.game.board_size
@@ -154,18 +170,17 @@ class BadukPanWidget(Widget):
board_height_with_margins = y_grid_spaces * self.grid_size
extra_px_margin_x = (self.width - board_width_with_margins) / 2
extra_px_margin_y = (self.height - board_height_with_margins) / 2
self.stone_size = self.grid_size * self.ui_config["stone_size"]
self.stone_size = self.grid_size * STONE_SIZE
self.gridpos_x = [self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5) for i in range(board_size_x)]
self.gridpos_y = [self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * self.grid_size + 0.5) for i in range(board_size_y)]
Color(*self.ui_config["board_color"])
Color(*BOARD_COLOR)
Rectangle(
pos=(self.gridpos_x[0] - self.grid_size * 1.5, self.gridpos_y[0] - self.grid_size * 1.5), size=(self.grid_size * x_grid_spaces, self.grid_size * y_grid_spaces)
)
line_color = self.ui_config["line_color"]
Color(*line_color)
Color(*LINE_COLOR)
for i in range(board_size_x):
Line(points=[(self.gridpos_x[i], self.gridpos_y[0]), (self.gridpos_x[i], self.gridpos_y[-1])])
for i in range(board_size_y):
@@ -178,10 +193,10 @@ class BadukPanWidget(Widget):
return []
return [star_point_pos - 1, size - star_point_pos] + ([int(size / 2)] if size % 2 == 1 and size > 7 else [])
starpt_size = self.grid_size * self.ui_config["starpoint_size"]
starpt_size = self.grid_size * STARPOINT_SIZE
for x in star_point_coords(board_size_x):
for y in star_point_coords(board_size_y):
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, line_color)
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, LINE_COLOR)
# coordinates
Color(0.25, 0.25, 0.25)
@@ -192,10 +207,10 @@ class BadukPanWidget(Widget):
draw_text(pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5)
def draw_board_contents(self, *_args):
if not self.ui_config:
if not (self.katrain and self.katrain.game):
return
stone_color = self.ui_config["stones"]
outline_color = self.ui_config["outline"]
stone_color = STONE_COLORS
outline_color = OUTLINE_COLORS
katrain = self.katrain
board_size_x, board_size_y = katrain.game.board_size
lock_ai = self.trainer_config.get("lock_ai", False) and katrain.controls.teaching_mode_enabled()
@@ -212,7 +227,7 @@ class BadukPanWidget(Widget):
for m in katrain.game.stones:
has_stone[m.coords] = m.player
show_dots_for = {p: self.trainer_config["eval_show_ai"] or katrain.controls.player_mode(p) != PLAYER_AI for p in Move.PLAYERS}
show_dots_for = {p: self.trainer_config["eval_show_ai"] or katrain.game.players[p].human for p in Move.PLAYERS}
nodes = katrain.game.current_node.nodes_from_root
realized_points_lost = None
for i, node in enumerate(nodes[::-1]): # reverse order!
@@ -257,13 +272,7 @@ class BadukPanWidget(Widget):
Rectangle(pos=(self.gridpos_x[x] - rsz / 2, self.gridpos_y[y] - rsz / 2), size=(rsz, rsz))
policy = current_node.policy
if (
not policy
and current_node.parent
and current_node.parent.policy
and katrain.controls.player_mode("B") == PLAYER_AI
and katrain.controls.player_mode("W") == PLAYER_AI
):
if not policy and current_node.parent and current_node.parent.policy and katrain.game.last_player.ai and katrain.game.next_player.ai:
policy = current_node.parent.policy # in the case of AI self-play we allow the policy to be one step out of date
pass_btn = katrain.board_controls.pass_btn
@@ -276,13 +285,13 @@ class BadukPanWidget(Widget):
if policy_grid[y][x] > 0:
polsize = 1.1 * math.sqrt(policy_grid[y][x])
policy_circle_color = (
*self.ui_config["policy_color"],
self.ui_config["ghost_alpha"] + self.ui_config["top_move_x_alpha"] * (policy_grid[y][x] == best_move_policy),
*POLICY_COLOR,
GHOST_ALPHA + TOP_MOVE_ALPHA * (policy_grid[y][x] == best_move_policy),
)
self.draw_stone(x, y, policy_circle_color, scale=polsize)
polsize = math.sqrt(policy[-1])
with pass_btn.canvas.after:
draw_circle((pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), polsize * pass_btn.height / 2, self.ui_config["policy_color"])
draw_circle((pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), polsize * pass_btn.height / 2, POLICY_COLOR)
# pass circle
passed = len(nodes) > 1 and current_node.is_pass
@@ -301,21 +310,20 @@ class BadukPanWidget(Widget):
self.draw_hover_contents()
def draw_hover_contents(self, *_args):
ghost_alpha = self.ui_config["ghost_alpha"]
ghost_alpha = GHOST_ALPHA
katrain = self.katrain
game_ended = katrain.game.ended
current_node = katrain.game.current_node
player, next_player = current_node.player, current_node.next_player
stone_color = self.ui_config["stones"]
teaching = katrain.controls.teaching_mode_enabled()
lock_ai = self.trainer_config.get("lock_ai", False) and teaching
stone_color = STONE_COLORS
lock_ai = self.trainer_config.get("lock_ai", False) and self.katrain.controls.play_analyze_mode == MODE_PLAY
self.canvas.after.clear()
with self.canvas.after:
self.active_pv_moves = []
# children of current moves in undo / review
alpha = self.ui_config["ghost_alpha"]
alpha = GHOST_ALPHA
if katrain.analysis_controls.show_children.active:
for child_node in current_node.children:
points_lost = child_node.points_lost
@@ -326,10 +334,9 @@ class BadukPanWidget(Widget):
else:
evalcol = copy.copy(self.eval_color(points_lost))
evalcol[3] = alpha
# if ((teaching and child_node.auto_undo) or katrain.controls.play_analyze_mode == "analyze") and child_node.analysis_ready: # TODO:?
if child_node.analysis_ready:
self.active_pv_moves.append((move.coords, [move.gtp()] + child_node.candidate_moves[0]["pv"], current_node))
scale = self.ui_config["child_scale"]
scale = CHILD_SCALE
self.draw_stone(move.coords[0], move.coords[1], (*stone_color[move.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale)
# hints or PV
@@ -338,10 +345,10 @@ class BadukPanWidget(Widget):
for i, move_dict in enumerate(hint_moves):
move = Move.from_gtp(move_dict["move"])
if move.coords is not None:
alpha, scale = self.ui_config["ghost_alpha"], 1.0
alpha, scale = GHOST_ALPHA, 1.0
if i == 0:
alpha += self.ui_config["top_move_x_alpha"]
elif move_dict["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]:
alpha += TOP_MOVE_ALPHA
elif move_dict["visits"] < VISITS_FRAC_SMALL * hint_moves[0]["visits"]:
scale = 0.8
if "pv" in move_dict:
self.active_pv_moves.append((move.coords, move_dict["pv"], current_node))
@@ -356,7 +363,7 @@ class BadukPanWidget(Widget):
animating_pv = self.animating_pv
if animating_pv:
pv, node, start_time, _ = animating_pv
delay = self.ui_config.get("anim_pv_time", 0.5)
delay = self.trainer_config.get("anim_pv_time", 0.5)
up_to_move = (time.time() - start_time) / delay
self.draw_pv(pv, node, up_to_move)
@@ -367,7 +374,7 @@ class BadukPanWidget(Widget):
def draw_pv(self, pv, node, up_to_move):
katrain = self.katrain
next_last_player = [node.next_player, node.player]
stone_color = self.ui_config["stones"]
stone_color = STONE_COLORS
cn = katrain.game.current_node
if node != cn and node.parent != cn:
hide_node = cn
@@ -401,18 +408,20 @@ class BadukPanWidget(Widget):
def show_pv_from_comments(self, pv_str):
self.set_animating_pv(pv_str[1:].split(" "), self.katrain.controls.active_comment_node.parent)
class AnalysisControls(MDBoxLayout):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.analysis_menu=None
Clock.schedule_once(self.build_menu,0)
self.analysis_menu = None
Clock.schedule_once(self.build_menu, 0)
def build_menu(self,_dt):
def build_menu(self, _dt):
menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)]
self.analysis_menu = MDDropdownMenu(caller=self.analysis_button, items=menu_items, width_mult=4)
def open_analysis_menu(self):
self.analysis_menu.open()
class BadukPanControls(MDFloatLayout):
pass
+35 -55
View File
@@ -6,7 +6,7 @@ from kivy.uix.boxlayout import BoxLayout
from kivy.uix.popup import Popup
from kivymd.uix.boxlayout import MDBoxLayout
from katrain.core.common import PLAYER_HUMAN, PLAYER_AI, PLAYER_HUMAN_TEACHING
from katrain.core.common import MODE_PLAY, MODE_ANALYZE
from katrain.gui.popups import ConfigTeacherPopup, ConfigTimerPopup
from katrain.gui.ai_settings import ConfigAIPopupContents
@@ -28,7 +28,6 @@ class ControlsPanel(BoxLayout):
self.active_comment_node = None
self.timer_settings_popup = None
self.last_timer_update = (None, 0)
self.periods_used = {"B": 0, "W": 0}
Clock.schedule_interval(self.update_timer, 0.07)
def check_hide_show(self, *_args):
@@ -42,66 +41,47 @@ class ControlsPanel(BoxLayout):
@property
def play_analyze_mode(self):
return "analyze" # ??
def player_mode(self, player):
return PLAYER_HUMAN
def ai_mode(self, player):
return self.ai_mode_groups[player].text
def teaching_mode_enabled(self):
return PLAYER_HUMAN_TEACHING in [self.player_mode("B"), self.player_mode("W")]
def on_size(self, *args):
self.update_evaluation()
return MODE_ANALYZE # ??
# handles showing completed analysis and score graph
def update_evaluation(self):
katrain = self.katrain
current_node = katrain and katrain.game and katrain.game.current_node
if current_node is not self.status_node and not (self.status is not None and self.status_node is None and current_node.is_root): # startup errors on root
game = katrain and katrain.game
if not game:
return
current_node, move = game.current_node, game.current_node.move
if game.current_node is not self.status_node and not (self.status is not None and self.status_node is None and game.current_node.is_root): # startup errors on root
self.status.text = ""
self.status_node = None
info = ""
both_players_are_robots = all(p.ai for p in game.players.values())
last_player_was_ai_playing_human = game.last_player.ai and game.next_player.human
if current_node:
hints = katrain.analysis_controls.hints.active
move = current_node.move
both_players_are_robots = self.player_mode(current_node.player) == PLAYER_AI and self.player_mode(current_node.next_player) == PLAYER_AI
last_player_was_human_or_both_robots = current_node.player and (self.player_mode(current_node.player) != PLAYER_AI or both_players_are_robots)
last_player_was_ai_playing_human = (
current_node.player and self.player_mode(current_node.player) == PLAYER_AI and self.player_mode(current_node.next_player) != PLAYER_AI
)
if last_player_was_human_or_both_robots and not current_node.is_root and move:
info += current_node.comment(teach=self.player_mode(current_node.player) == PLAYER_HUMAN_TEACHING, hints=hints)
self.active_comment_node = current_node
elif last_player_was_ai_playing_human and current_node.parent:
info += current_node.parent.comment(teach=self.player_mode(current_node.next_player) == PLAYER_HUMAN_TEACHING, hints=hints)
self.active_comment_node = current_node
if self.play_analyze_mode == MODE_PLAY and last_player_was_ai_playing_human:
if game.next_player.being_taught and current_node.children and current_node.children.auto_undo:
self.active_comment_node = current_node.children[-1]
elif current_node.parent:
self.active_comment_node = current_node.parent
if current_node.analysis_ready:
self.stats.score.text = current_node.format_score()
self.stats.winrate.text = current_node.format_winrate()
if move and last_player_was_human_or_both_robots: # don't immediately hide this when an ai moves comes in
points_lost = current_node.points_lost
self.stats.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained"
self.stats.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "-"
elif not last_player_was_ai_playing_human:
self.stats.score_change.label = f"Points lost"
self.stats.score_change.text = "-"
elif last_player_was_ai_playing_human and current_node.parent and current_node.parent.move:
points_lost = current_node.parent.points_lost
self.stats.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained"
self.stats.score_change.text = f"{current_node.parent.move.player}: {abs(points_lost):.1f}" if points_lost else "-"
elif both_players_are_robots and current_node.parent and current_node.parent.analysis_ready:
self.stats.score.text = current_node.parent.format_score()
self.stats.winrate.text = current_node.parent.format_winrate()
hints = katrain.analysis_controls.hints.active
info = ""
if current_node.move and not current_node.is_root:
info = self.active_comment_node.comment(teach=game.players[self.active_comment_node.player].being_taught, hints=hints)
self.graph.update_value(current_node)
self.note.text = current_node.note
if self.active_comment_node.analysis_ready:
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
self.stats.player = self.active_comment_node.player
else:
self.stats.score = ""
self.stats.winrate = ""
self.stats.points_lost = None
self.stats.player = ""
self.graph.update_value(current_node)
self.note.text = current_node.note
self.info.text = info
def configure_ais(self):
@@ -122,10 +102,10 @@ class ControlsPanel(BoxLayout):
last_update_node, last_update_time = self.last_timer_update
now = time.time()
self.last_timer_update = (current_node, now)
player = current_node.next_player
byo_len = max(1, self.katrain.config("timer/byo_length"))
byo_num = max(1, self.katrain.config("timer/byo_num"))
ai = self.player_mode(player) == PLAYER_AI
player = self.katrain.game.next_player
ai = player.ai
if not self.timer.paused and not ai:
if last_update_node == current_node and not current_node.children:
current_node.time_used += now - last_update_time
@@ -135,9 +115,9 @@ class ControlsPanel(BoxLayout):
while time_remaining < 0:
current_node.time_used -= byo_len
time_remaining += byo_len
self.periods_used[player] += 1
player.periods_used += 1
time_remaining = byo_len - current_node.time_used
periods_rem = byo_num - self.periods_used[player]
periods_rem = byo_num - player.periods_used
self.timer.state = (time_remaining, periods_rem, ai)
def configure_timer(self):
+11 -9
View File
@@ -33,10 +33,10 @@ class ScoreGraph(BackgroundColor):
self.nodes.append(node)
self.highlighted_index = 0
def show_graphs(self,keys):
self.show_score = keys['score']
self.show_winrate = keys['winrate']
self.show_pointloss = keys['points']
def show_graphs(self, keys):
self.show_score = keys["score"]
self.show_winrate = keys["winrate"]
self.show_pointloss = keys["points"]
def update_graph(self, *args):
nodes = self.nodes
@@ -45,21 +45,23 @@ class ScoreGraph(BackgroundColor):
score_nn_values = [n.score for n in nodes if n and n.score]
score_values_range = min(score_nn_values or [0]), max(score_nn_values or [0])
winrate_values = [(n.winrate-0.5)*100 if n and n.winrate else math.nan for n in nodes]
winrate_nn_values = [(n.winrate-0.5)*100 for n in nodes if n and n.winrate]
winrate_values = [(n.winrate - 0.5) * 100 if n and n.winrate else math.nan for n in nodes]
winrate_nn_values = [(n.winrate - 0.5) * 100 for n in nodes if n and n.winrate]
winrate_values_range = min(winrate_nn_values or [0]), max(winrate_nn_values or [0])
score_granularity = 5
winrate_granularity = 10
self.score_scale = max(math.ceil( max(-score_values_range[0], score_values_range[1]) / score_granularity),1) * score_granularity
self.score_scale = max(math.ceil(max(-score_values_range[0], score_values_range[1]) / score_granularity), 1) * score_granularity
self.winrate_scale = max(math.ceil(max(-winrate_values_range[0], winrate_values_range[1]) / winrate_granularity), 1) * winrate_granularity
xscale = self.width / max(len(score_values) - 1, 15)
available_height = self.height
score_line_points = [[self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.score_scale)] for i, val in enumerate(score_values)]
winrate_line_points = [[self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.winrate_scale)] for i, val in enumerate(winrate_values)]
winrate_line_points = [
[self.pos[0] + i * xscale, self.pos[1] + self.height / 2 + available_height / 2 * (val / self.winrate_scale)] for i, val in enumerate(winrate_values)
]
self.score_points = sum(score_line_points, [])
self.winrate_points = sum(winrate_line_points,[])
self.winrate_points = sum(winrate_line_points, [])
if self.highlighted_index is not None:
self.highlighted_index = min(self.highlighted_index, len(score_values) - 1)
+36 -23
View File
@@ -23,13 +23,14 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import BasePressedButton, BaseFlatButton
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.navigationdrawer import MDNavigationDrawer
from katrain.core.common import i18n, I18NLabel, I18NTextInput, I18NCoreLabel
from kivy.core.text import Label as CoreLabel
from katrain.core.common import i18n, I18NTextInput, I18NLabel
#
# --- new mixins
class BackgroundColor(Widget):
background_color = ListProperty([1, 1, 1, 0])
@@ -89,14 +90,16 @@ class SizedMDFlatRectangleToggleButton(SizedMDFlatRectangleButton, ToggleButtonB
return self.state == "down"
return self.state == "down"
class AutoSizedMDFlatRectangleButton(SizedMDFlatRectangleButton):
hor_padding = NumericProperty(3)
class AutoSizedMDFlatRectangleToggleButton(SizedMDFlatRectangleToggleButton):
hor_padding = NumericProperty(3)
class TransparentIconButton(CircularRippleBehavior,Button):
class TransparentIconButton(CircularRippleBehavior, Button):
icon_size = ListProperty([25, 25])
icon = StringProperty("")
@@ -123,7 +126,7 @@ class MyNavigationDrawer(MDNavigationDrawer): # in PR - closes NavDrawer on any
return super().on_touch_up(touch)
class CircleWithText(MDFloatLayout):
class CircleWithText(Widget):
text = StringProperty("0")
player = OptionProperty("Black", options=["Black", "White"])
min_size = NumericProperty(50)
@@ -156,7 +159,7 @@ class CollapsablePanel(MDBoxLayout):
option_labels = ListProperty([])
option_default_active = ListProperty([])
option_colors = ListProperty([])
closed_label = StringProperty('Closed Panel')
closed_label = StringProperty("Closed Panel")
size_hint_y_open = NumericProperty(1)
height_open = NumericProperty(None)
@@ -170,44 +173,48 @@ class CollapsablePanel(MDBoxLayout):
self.option_buttons = []
super().__init__(**kwargs)
self.orientation = "vertical"
self.bind(options=self.build_options, option_colors=self.build_options, options_height=self.build_options,
option_default_active=self.build_options,options_left_padding=self.build_options)
self.bind(state=self.build,size_hint_y_open=self.build,height_open=self.build)
self.bind(
options=self.build_options,
option_colors=self.build_options,
options_height=self.build_options,
option_default_active=self.build_options,
options_left_padding=self.build_options,
)
self.bind(state=self.build, size_hint_y_open=self.build, height_open=self.build)
self.build_options()
def build_options(self, *args, **kwargs):
self.header = MDBoxLayout(height=self.options_height,size_hint_y=None,
padding=[self.options_left_padding, 0, 0, 0], spacing=3)
self.header = MDBoxLayout(height=self.options_height, size_hint_y=None, padding=[self.options_left_padding, 0, 0, 0], spacing=3)
self.option_buttons = []
option_labels = self.option_labels or [i18n._(f"tab:{opt}") for opt in self.options]
for lbl, opt_col, active in zip(option_labels, self.option_colors, self.option_default_active):
button = AutoSizedMDFlatRectangleToggleButton(text=lbl,color=opt_col, height=self.options_height,
on_press=self.trigger_select, state="down" if active else "normal")
button = AutoSizedMDFlatRectangleToggleButton(text=lbl, color=opt_col, height=self.options_height, on_press=self.trigger_select, state="down" if active else "normal")
self.option_buttons.append(button)
self.open_close_button = TransparentIconButton( # << / >> collapse button
self.open_close_button = TransparentIconButton( # << / >> collapse button
icon=self.open_close_icon(),
icon_size=[0.5 * self.options_height, 0.5 * self.options_height],
width=0.75*self.options_height,size_hint_x=None,
on_press=lambda *_args: self.set_state('toggle'),
width=0.75 * self.options_height,
size_hint_x=None,
on_press=lambda *_args: self.set_state("toggle"),
)
self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None,self.open_close_icon()))
self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None, self.open_close_icon()))
self.build()
def build(self, *args, **kwargs):
self.header.clear_widgets()
if self.state=='open':
if self.state == "open":
for button in self.option_buttons:
self.header.add_widget(button)
self.header.add_widget(Label()) # spacer
self.header.add_widget(Label()) # spacer
self.trigger_select()
else:
self.header.add_widget(I18NLabel(text=i18n._(self.closed_I18NLabel),halign='right',height=self.options_height))
self.header.add_widget(I18NLabel(text=i18n._(self.closed_label), halign="right", height=self.options_height))
self.header.add_widget(self.open_close_button)
super().clear_widgets()
super().add_widget(self.header)
height, size_hint_y = 1, None
if self.state=='open' and self.contents:
if self.state == "open" and self.contents:
super().add_widget(self.contents)
if self.height_open:
height = self.height_open
@@ -215,7 +222,7 @@ class CollapsablePanel(MDBoxLayout):
size_hint_y = self.size_hint_y_open
else:
height = self.header.height
self.height, self.size_hint_y = height,size_hint_y
self.height, self.size_hint_y = height, size_hint_y
def open_close_icon(self):
return self.open_icon if self.state == "open" else self.close_icon
@@ -236,12 +243,18 @@ class CollapsablePanel(MDBoxLayout):
def trigger_select(self, *_args):
if self.state == "open":
self.dispatch("on_option_state",{opt:btn.state == "down" for opt,btn in zip(self.options,self.option_buttons)})
self.dispatch("on_option_state", {opt: btn.state == "down" for opt, btn in zip(self.options, self.option_buttons)})
def on_option_state(self, options):
pass
class StatsBox(MDBoxLayout, BackgroundColor):
winrate = StringProperty("...")
score = StringProperty("...")
points_lost = NumericProperty(None, allownone=True)
player = StringProperty("")
# --- not checked
@@ -469,7 +482,7 @@ class LabelledIntInput(LabelledTextInput):
def draw_text(pos, text, **kw):
label = I18NCoreLabel(text=text, bold=True, **kw)
label = CoreLabel(text=text, bold=True, font_name=i18n._("Roboto"), **kw) #
label.refresh()
Rectangle(texture=label.texture, pos=(pos[0] - label.texture.size[0] / 2, pos[1] - label.texture.size[1] / 2), size=label.texture.size)
+29
View File
@@ -0,0 +1,29 @@
# eval dots
EVAL_COLORS = [
[0.447, 0.129, 0.42, 1],
[0.8, 0, 0, 1],
[0.9, 0.4, 0.1, 1],
[0.85, 0.89, 0.3, 1],
[0.67, 0.9, 0.18, 1.0],
[0.117, 0.588, 0, 1.0],
]
EVAL_DOT_MAX_SIZE = 0.5
EVAL_DOT_MIN_SIZE = 0.25
# board
LINE_COLOR = [0, 0, 0]
POLICY_COLOR = [0.9, 0.2, 0.8]
STARPOINT_SIZE = 0.1
BOARD_COLOR = [0.85, 0.68, 0.40, 1]
STONE_SIZE = 0.475
VISITS_FRAC_SMALL = 0.1
STONE_COLORS = {"B": [0.05, 0.05, 0.05], "W": [0.95, 0.95, 0.95]}
OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]}
GHOST_ALPHA = 0.5
TOP_MOVE_ALPHA = 0.3
CHILD_SCALE = 0.95
# ponder dot
ENGINE_DOWN_COL = [0.8, 0, 0, 1]
ENGINE_BUSY_COL = [0.9, 0.4, 0.1, 1]
ENGINE_READY_COL = [0.117, 0.588, 0, 1]