move tree support
This commit is contained in:
1 parent
c3eff3f0f5
commit
76c982536c
8 files changed
+199
-59
No files matched your search
+12
-8
@@ -68,6 +68,7 @@ from katrain.gui.kivyutils import *
|
||||
from katrain.gui.popups import ConfigPopup, LoadSGFPopup, NewGamePopup, AIPopup
|
||||
from katrain.gui.style import ENGINE_BUSY_COL, ENGINE_DOWN_COL, ENGINE_READY_COL, LIGHTGREY
|
||||
from katrain.gui.widgets.graph import ScoreGraph
|
||||
from katrain.gui.widgets.movetree import MoveTree
|
||||
from katrain.gui.widgets.filebrowser import I18NFileBrowser
|
||||
from katrain.gui.badukpan import AnalysisControls, BadukPanControls, BadukPanWidget
|
||||
from katrain.gui.controlspanel import ControlsPanel
|
||||
@@ -148,12 +149,15 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
else:
|
||||
self.board_controls.engine_status_col = ENGINE_BUSY_COL
|
||||
|
||||
# redraw
|
||||
# redraw board/stones
|
||||
if redraw_board:
|
||||
self.board_gui.draw_board()
|
||||
self.board_gui.redraw_board_contents_trigger()
|
||||
self.controls.update_evaluation()
|
||||
self.controls.update_timer(1)
|
||||
# update move tree
|
||||
self.controls.move_tree.current_node = self.game.current_node
|
||||
|
||||
|
||||
def update_state(
|
||||
self, redraw_board=False
|
||||
@@ -242,7 +246,7 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
def _do_undo(self, n_times=1):
|
||||
if n_times == "smart":
|
||||
n_times = 1
|
||||
if self.play_analyze_mode == "play" and self.last_player_info.ai and self.next_player_info.human:
|
||||
if self.play_analyze_mode == MODE_PLAY and self.last_player_info.ai and self.next_player_info.human:
|
||||
n_times = 2
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.undo(n_times)
|
||||
@@ -251,9 +255,9 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.redo(n_times)
|
||||
|
||||
def _do_switch_branch(self, direction):
|
||||
def _do_switch_branch(self, *args):
|
||||
self.board_gui.animating_pv = None
|
||||
self.game.switch_branch(direction)
|
||||
self.game.switch_branch(*args)
|
||||
|
||||
def _do_play(self, coords):
|
||||
self.board_gui.animating_pv = None
|
||||
@@ -386,8 +390,8 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
"s": ("analyze-extra", "equalize"),
|
||||
"d": ("analyze-extra", "sweep"),
|
||||
"p": ("play", None),
|
||||
"right": ("switch-branch", 1),
|
||||
"left": ("switch-branch", -1),
|
||||
"down": ("switch-branch", 1),
|
||||
"up": ("switch-branch", -1),
|
||||
"f5": ("timer-popup",),
|
||||
"f6": ("teacher-popup",),
|
||||
"f7": ("ai-popup",),
|
||||
@@ -420,9 +424,9 @@ class KaTrainGui(Screen, KaTrainBase):
|
||||
self.controls.timer.paused = not self.controls.timer.paused
|
||||
elif keycode[1] in ["`", "~", "m"]:
|
||||
self.zen = (self.zen + 1) % 3
|
||||
elif keycode[1] in ["up", "z"]:
|
||||
elif keycode[1] in ["left", "z"]:
|
||||
self("undo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] in ["down", "x"]:
|
||||
elif keycode[1] in ["right", "x"]:
|
||||
self("redo", 1 + ("shift" in modifiers) * 9 + ("ctrl" in modifiers) * 999)
|
||||
elif keycode[1] == "n" and "ctrl" in modifiers:
|
||||
self("new-game-popup")
|
||||
|
||||
@@ -173,11 +173,16 @@ class Game:
|
||||
cn = cn.favourite_child
|
||||
self.set_current_node(cn)
|
||||
|
||||
def switch_branch(self, direction):
|
||||
def switch_branch(self, direction, cycle=False):
|
||||
cn = self.current_node # avoid race conditions
|
||||
if cn.parent and len(cn.parent.children) > 1:
|
||||
ix = cn.parent.children.index(cn)
|
||||
self.set_current_node(cn.parent.children[(ix + direction) % len(cn.parent.children)])
|
||||
ordered_children = GameNode.order_children(cn.parent.children)
|
||||
ix = ordered_children.index(cn) + direction
|
||||
if cycle:
|
||||
ix = (ix + len(ordered_children)) % len(ordered_children)
|
||||
elif ix < 0 or ix >= len(ordered_children):
|
||||
return
|
||||
self.set_current_node(ordered_children[ix])
|
||||
|
||||
def place_handicap_stones(self, n_handicaps):
|
||||
board_size_x, board_size_y = self.board_size
|
||||
|
||||
+43
-39
@@ -1,39 +1,34 @@
|
||||
#:kivy 1.11.0
|
||||
#:import i18n katrain.core.lang.i18n
|
||||
|
||||
# TODO: refactor
|
||||
#:set CP_SPACING 6
|
||||
#:set CP_SMALL_SPACING 3
|
||||
#:set CP_PADDING 6
|
||||
|
||||
#:import PLAYER_TYPES katrain.core.constants.PLAYER_TYPES
|
||||
#:import PLAYER_HUMAN katrain.core.constants.PLAYER_HUMAN
|
||||
#:import PLAYER_AI katrain.core.constants.PLAYER_AI
|
||||
#:import MODE_PLAY katrain.core.constants.MODE_PLAY
|
||||
#:import MODE_ANALYZE katrain.core.constants.MODE_ANALYZE
|
||||
|
||||
#:import RED katrain.gui.style.RED
|
||||
#:import WHITE katrain.gui.style.WHITE
|
||||
#:import BLACK katrain.gui.style.BLACK
|
||||
#:import LIGHTGREY katrain.gui.style.LIGHTGREY
|
||||
#:import GREEN katrain.gui.style.GREEN
|
||||
#:import YELLOW katrain.gui.style.YELLOW
|
||||
#:import RED katrain.gui.style.RED
|
||||
#:import DARKRED katrain.gui.style.DARKRED
|
||||
#:import ORANGE katrain.gui.style.ORANGE
|
||||
|
||||
#:import BACKGROUND_COLOR katrain.gui.style.BACKGROUND_COLOR
|
||||
#:import BOX_BACKGROUND_COLOR katrain.gui.style.BOX_BACKGROUND_COLOR
|
||||
#:import LIGHTER_BACKGROUND_COLOR katrain.gui.style.LIGHTER_BACKGROUND_COLOR
|
||||
#:import TEXT_COLOR katrain.gui.style.TEXT_COLOR
|
||||
|
||||
#:import SCORE_COLOR katrain.gui.style.SCORE_COLOR
|
||||
#:import WINRATE_COLOR katrain.gui.style.WINRATE_COLOR
|
||||
#:import POINTLOSS_COLOR katrain.gui.style.POINTLOSS_COLOR
|
||||
|
||||
#:set SPINNER_OPTION_COLOR [64 / 255, 85 / 255, 110 / 255, 1]
|
||||
|
||||
#:set BLACK [0.05,0.05,0.05,1]
|
||||
#:set GREEN [0.1,0.8,0.1,1]
|
||||
#:set YELLOW [0.8,0.8,0.1,1]
|
||||
#:set DARKRED [0.3,0.1,0.1,1]
|
||||
#:set ORANGE [242/255,96/255,34/255,1]
|
||||
|
||||
#:set BUTTON_INACTIVE_COLOR LIGHTGREY
|
||||
#:set NOTES_FONT_SIZE dp(18)
|
||||
|
||||
#:set CP_SPACING dp(6)
|
||||
#:set CP_SMALL_SPACING dp(3)
|
||||
#:set CP_PADDING dp(6)
|
||||
#:set NOTES_FONT_SIZE dp(18)
|
||||
|
||||
# for sizing help
|
||||
#<Label>:
|
||||
@@ -129,7 +124,7 @@
|
||||
|
||||
|
||||
<I18NSpinnerOption@SpinnerOption>:
|
||||
background_color: SPINNER_OPTION_COLOR
|
||||
background_color: LIGHTER_BACKGROUND_COLOR
|
||||
background_normal: ''
|
||||
lang_change_tracking: i18n._('') # for font
|
||||
color: WHITE
|
||||
@@ -241,7 +236,7 @@
|
||||
NavIconButton:
|
||||
id: branch
|
||||
icon: 'img/Branch.png'
|
||||
on_press: root.katrain('switch-branch',1)
|
||||
on_press: root.katrain('switch-branch',1,True)
|
||||
disabled: True
|
||||
ClickableLabel:
|
||||
pos_hint: {'center_x': 0.85,'center_y':0.5}
|
||||
@@ -350,7 +345,6 @@
|
||||
TimerLabel:
|
||||
color: BACKGROUND_COLOR
|
||||
text: '8:88'
|
||||
text_size: self.size
|
||||
font_size: 0.85*self.height
|
||||
pos_hint: {'right':1,'center_y':0.5}
|
||||
halign: 'right'
|
||||
@@ -359,7 +353,6 @@
|
||||
id: timer
|
||||
timeout: root.timeout
|
||||
text: "{:01d}:{:02d}".format(int(root.state[0]+0.99)//60,int((root.state[0]+0.99) % 60) )
|
||||
text_size: self.size
|
||||
font_size: 0.85*self.height
|
||||
pos_hint: {'right':1,'center_y':0.5}
|
||||
halign: 'right'
|
||||
@@ -487,6 +480,7 @@
|
||||
width: 1
|
||||
points: (*self.pos,self.x+self.width,self.y)
|
||||
|
||||
|
||||
<ControlsPanel>:
|
||||
katrain: app.gui
|
||||
orientation: 'vertical'
|
||||
@@ -498,6 +492,8 @@
|
||||
info: info
|
||||
note: note
|
||||
status: status
|
||||
timer_or_movetree: timer_or_movetree
|
||||
move_tree: move_tree
|
||||
padding: CP_PADDING
|
||||
spacing: CP_SPACING
|
||||
player_box_height: max(50, 0.075 * root.height)
|
||||
@@ -514,26 +510,34 @@
|
||||
PlayerInfo:
|
||||
id: W_player
|
||||
player: 'W'
|
||||
BoxLayout:
|
||||
TimerOrMoveTree:
|
||||
id: timer_or_movetree
|
||||
size_hint_y: None
|
||||
height: root.timer_box_height
|
||||
spacing: CP_SPACING
|
||||
Timer:
|
||||
id: timer
|
||||
size_hint_x: 0.7
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: 2*CP_PADDING,CP_PADDING
|
||||
BoxLayout:
|
||||
size_hint_x: 0.0001 if self.parent.mode != MODE_PLAY else 1
|
||||
opacity: 0 if self.parent.mode != MODE_PLAY else 1
|
||||
spacing: CP_SPACING
|
||||
size_hint_x: 0.3
|
||||
SizedRoundedRectangleButton:
|
||||
text: i18n._('pass-button-text')
|
||||
size_hint: 1, 0.5
|
||||
on_left_release: root.katrain("play",None)
|
||||
SizedRoundedRectangleButton:
|
||||
text: i18n._('undo-button-text')
|
||||
size_hint: 1, 0.5
|
||||
on_left_release: root.katrain("undo","smart")
|
||||
Timer:
|
||||
id: timer
|
||||
size_hint_x: 0.7
|
||||
MDBoxLayout:
|
||||
orientation: 'vertical'
|
||||
padding: 2*CP_PADDING,CP_PADDING
|
||||
spacing: CP_SPACING
|
||||
size_hint_x: 0.3
|
||||
SizedRoundedRectangleButton:
|
||||
text: i18n._('pass-button-text')
|
||||
size_hint: 1, 0.5
|
||||
on_left_release: root.katrain("play",None)
|
||||
SizedRoundedRectangleButton:
|
||||
text: i18n._('undo-button-text')
|
||||
size_hint: 1, 0.5
|
||||
on_left_release: root.katrain("undo","smart")
|
||||
MoveTree:
|
||||
id: move_tree
|
||||
size_hint_x: 0.1 if self.parent.mode == MODE_PLAY else 1
|
||||
opacity: 0 if self.parent.mode == MODE_PLAY else 1
|
||||
CollapsablePanel:
|
||||
id: graph_panel
|
||||
height_open: root.graph_box_height
|
||||
@@ -673,7 +677,7 @@
|
||||
|
||||
<AnalysisMenuItem@MenuItem>:
|
||||
on_close: app.gui.analysis_controls.close_dropdown()
|
||||
background_color: SPINNER_OPTION_COLOR
|
||||
background_color: LIGHTER_BACKGROUND_COLOR
|
||||
height: 45
|
||||
canvas.before:
|
||||
Color:
|
||||
|
||||
@@ -438,7 +438,6 @@ class BadukPanWidget(Widget):
|
||||
if i > up_to_move:
|
||||
return
|
||||
move_player = next_last_player[i % 2]
|
||||
opp_player = next_last_player[1 - i % 2]
|
||||
coords = Move.from_gtp(gtpmove).coords
|
||||
if coords is None: # tee-hee
|
||||
sizefac = katrain.board_controls.pass_btn.size[1] / 2 / self.stone_size
|
||||
@@ -453,7 +452,7 @@ class BadukPanWidget(Widget):
|
||||
sizefac = 1
|
||||
|
||||
draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player])
|
||||
Color(*stone_color[opp_player])
|
||||
Color(*STONE_TEXT_COLORS[move_player])
|
||||
draw_text(pos=board_coords, text=str(i + 1), font_size=self.grid_size * sizefac / 1.45, font_name="Roboto")
|
||||
|
||||
def set_animating_pv(self, pv, node):
|
||||
|
||||
@@ -47,6 +47,7 @@ class PlayAnalyzeSelect(MDFloatLayout):
|
||||
return
|
||||
self.save_ui_state()
|
||||
self.mode = new_mode
|
||||
self.katrain.controls.timer_or_movetree.mode = self.mode
|
||||
self.load_ui_state()
|
||||
self.katrain.update_state() # for lock ai even if nothing changed
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.button import BaseFlatButton, BasePressedButton
|
||||
from kivymd.uix.navigationdrawer import MDNavigationDrawer
|
||||
|
||||
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, PLAYER_AI
|
||||
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, GAME_TYPES, PLAYER_AI, MODE_PLAY
|
||||
from katrain.core.lang import i18n
|
||||
from katrain.gui.style import DEFAULT_FONT, WHITE
|
||||
|
||||
@@ -299,6 +299,8 @@ class PlayerInfo(MDBoxLayout, BackgroundMixin):
|
||||
player_subtype = StringProperty("")
|
||||
active = BooleanProperty(True)
|
||||
|
||||
class TimerOrMoveTree(BoxLayout):
|
||||
mode = StringProperty(MODE_PLAY)
|
||||
|
||||
class Timer(BGBoxLayout):
|
||||
state = ListProperty([30, 5, 1])
|
||||
|
||||
+16
-6
@@ -5,19 +5,26 @@ def to_hexcol(kivycol):
|
||||
DEFAULT_FONT = "fonts/NotoSans-Regular.ttf"
|
||||
|
||||
# basic color definitions
|
||||
RED = [0.8, 0.1, 0.1, 1]
|
||||
WHITE = [0.95, 0.95, 0.95, 1]
|
||||
BLACK = [0.05, 0.05, 0.05, 1]
|
||||
LIGHTGREY = [0.7, 0.7, 0.7, 1]
|
||||
|
||||
# gui colors
|
||||
RED = [0.8, 0.1, 0.1, 1]
|
||||
GREEN = [0.1,0.8,0.1,1]
|
||||
YELLOW = [0.8,0.8,0.1,1]
|
||||
DARKRED = [0.3,0.1,0.1,1]
|
||||
ORANGE = [242/255,96/255,34/255,1]
|
||||
|
||||
# gui colors
|
||||
BACKGROUND_COLOR = [36 / 255, 48 / 255, 62 / 255, 1]
|
||||
BOX_BACKGROUND_COLOR = [46 / 255, 65 / 255, 88 / 255, 1]
|
||||
LIGHTER_BACKGROUND_COLOR = [64 / 255, 85 / 255, 110 / 255, 1]
|
||||
TEXT_COLOR = WHITE
|
||||
|
||||
SCORE_COLOR = [0.3, 0.7, 0.9, 1]
|
||||
WINRATE_COLOR = [0.1, 0.8, 0.1, 1]
|
||||
POINTLOSS_COLOR = [0.8, 0.8, 0.1, 1]
|
||||
WINRATE_COLOR = GREEN
|
||||
POINTLOSS_COLOR = YELLOW
|
||||
|
||||
# eval dots
|
||||
EVAL_COLORS = [
|
||||
[0.447, 0.129, 0.42, 1],
|
||||
@@ -30,6 +37,10 @@ EVAL_COLORS = [
|
||||
EVAL_DOT_MAX_SIZE = 0.5
|
||||
EVAL_DOT_MIN_SIZE = 0.25
|
||||
|
||||
STONE_COLORS = {"B": BLACK, "W": WHITE}
|
||||
OUTLINE_COLORS = {"B": [0.3, 0.3, 0.3, 0.5], "W": [0.7, 0.7, 0.7, 0.5]}
|
||||
STONE_TEXT_COLORS= {"W": BLACK, "B": WHITE}
|
||||
|
||||
# board
|
||||
LINE_COLOR = [0, 0, 0]
|
||||
POLICY_COLOR = [0.9, 0.2, 0.8]
|
||||
@@ -37,8 +48,7 @@ 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.4
|
||||
CHILD_SCALE = 0.95
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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, DictProperty, NumericProperty
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.widget import Widget
|
||||
from kivymd.app import MDApp
|
||||
|
||||
from katrain.core.game_node import GameNode
|
||||
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
|
||||
|
||||
|
||||
class MoveTreeCanvas(Widget):
|
||||
move_xy_pos = DictProperty({})
|
||||
move_size = NumericProperty(5)
|
||||
move_tree = ObjectProperty(None)
|
||||
|
||||
def on_touch_up(self, touch):
|
||||
if touch.button=='left':
|
||||
node, (x,y) = min(self.move_xy_pos.items(),key=lambda n_xy: abs(n_xy[1][0]-touch.x)+abs(n_xy[1][1]-touch.y) )
|
||||
if max(abs(x-touch.x),abs(y-touch.y)) <= (self.move_size/2):
|
||||
katrain = MDApp.get_running_app().gui
|
||||
katrain.game.set_current_node(node)
|
||||
katrain.update_state()
|
||||
|
||||
def draw_move_tree(self, current_node):
|
||||
if not self.move_tree:
|
||||
return
|
||||
spacing = 5
|
||||
moves_vert = 3
|
||||
self.move_size = (self.move_tree.height - (moves_vert+1) * spacing) / moves_vert
|
||||
|
||||
root = current_node.root
|
||||
|
||||
move_pos = {root: (0,0)}
|
||||
stack = GameNode.order_children( root.children )[::-1]
|
||||
next_y_pos = defaultdict(int) # x pos -> max y pos
|
||||
|
||||
while stack:
|
||||
move = stack.pop()
|
||||
x = move.depth
|
||||
y = max(next_y_pos[x], move_pos[move.parent][1])
|
||||
next_y_pos[x] = y+1
|
||||
next_y_pos[x-1] = max(next_y_pos[x],next_y_pos[x-1])
|
||||
move_pos[move] = (x,y)
|
||||
for c in GameNode.order_children( move.children )[::-1]: # stack, so push top child last to process first
|
||||
stack.append(c)
|
||||
|
||||
def draw_stone(pos, player):
|
||||
draw_circle(pos,self.move_size/2-0.5,STONE_COLORS[player])
|
||||
Color(*STONE_TEXT_COLORS[player])
|
||||
Line(circle=(*pos, self.move_size/2), width=1)
|
||||
|
||||
def coord_pos(coord):
|
||||
return (coord + 0.5) * (spacing + self.move_size) + spacing / 2
|
||||
|
||||
self.width = coord_pos(max(x+0.5 for x, y in move_pos.values()))
|
||||
self.height = coord_pos(max(y+0.5 for x, y in move_pos.values()))
|
||||
|
||||
def xy_pos(x,y):
|
||||
return coord_pos(x), self.height - coord_pos(y)
|
||||
|
||||
self.move_xy_pos = {n: xy_pos(x,y) for n,(x,y) in move_pos.items() }
|
||||
|
||||
self.canvas.clear()
|
||||
with self.canvas:
|
||||
Color(*YELLOW)
|
||||
Rectangle(pos=[c-self.move_size/2-spacing/2 for c in self.move_xy_pos[current_node]],size=(self.move_size+spacing,self.move_size+spacing))
|
||||
Color(*LIGHTGREY)
|
||||
for node, (x,y) in self.move_xy_pos.items():
|
||||
for ci, c in enumerate(GameNode.order_children(node.children)):
|
||||
cx,cy = self.move_xy_pos[c]
|
||||
Line(points=[x,y,x,cy,cx,cy],width=1)
|
||||
|
||||
for node, pos in self.move_xy_pos.items():
|
||||
draw_stone(pos, node.player)
|
||||
text = str(node.depth)
|
||||
Color(*STONE_COLORS['W' if node.player=='B' else 'B'])
|
||||
draw_text(pos=pos, text=text, font_size=self.move_size * 1.75 / (1 + 1 * len(text)), font_name="Roboto")
|
||||
|
||||
self.move_tree.scroll_to_pixel(*self.move_xy_pos[current_node])
|
||||
|
||||
|
||||
class MoveTree(ScrollView,BackgroundMixin):
|
||||
current_node = ObjectProperty(None)
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.redraw_tree_trigger = Clock.create_trigger(lambda _dt: self.move_tree_canvas.draw_move_tree(self.current_node))
|
||||
self.bind(current_node=self.redraw_tree_trigger,size=self.redraw_tree_trigger)
|
||||
|
||||
def scroll_to_pixel(self, x, y):
|
||||
if not self._viewport:
|
||||
return
|
||||
vp = self._viewport
|
||||
if vp.width > self.width:
|
||||
sx = (x - self.width/2) / (vp.width - self.width)
|
||||
self.scroll_x =max(0, min(1, sx))
|
||||
if vp.height > self.height:
|
||||
sy = (y - self.height/2) / (vp.height - self.height)
|
||||
self.scroll_y = max(0, min(1, sy))
|
||||
|
||||
|
||||
Builder.load_string(
|
||||
"""
|
||||
<MoveTree>:
|
||||
background_color: BOX_BACKGROUND_COLOR
|
||||
move_tree_canvas: move_tree_canvas
|
||||
MoveTreeCanvas:
|
||||
move_tree: root
|
||||
id: move_tree_canvas
|
||||
size_hint: None, None
|
||||
""")
|
||||
Reference in new issue
Block a user