from enum import Enum
from nicegui import ui
from nicegui.elements.html import Html
from nicegui.functions.timer import Timer
Direction = Enum('Direction', ['Up', 'Down', 'Right', 'Left'])
class Position():
def __init__ (self, x, y):
self.x = x
self.y = y
class State():
def __init__(self):
self.iteration = 0
self.robot_pos = Position(0, 0)
self.ball_pos = Position(0, 0)
self.grid_size = 0
self.cell_size = 0
self.solution_path = ''
self.start_pos = Position(0, 0)
self.gate = (Position(0,0), Position(0, 0))
self.ball_color = 'black'
self.robot_color = 'black'
self.path_color = 'black'
self.gate_color = 'black'
def build_svg( state: State) -> str:
cell_size = state.cell_size
grid_size = state.grid_size
width = cell_size * grid_size + 1
height = cell_size * grid_size + 1
current_path = state.solution_path.split(' ')[0:state.iteration]
# draw solution path
path = draw_path(state.start_pos.x, state.start_pos.y, current_path, grid_size, cell_size, state.path_color)
# draw gate
if(state.gate[0].x == state.gate[1].x):
gate_start_x = state.gate[0].x
gate_start_y = min(state.gate[0].y, state.gate[1].y)
gate_path = 'U' * abs(state.gate[0].y - state.gate[1].y)
else:
gate_start_x = min(state.gate[0].x, state.gate[1].x)
gate_start_y = state.gate[0].y
gate_path = 'R' * abs(state.gate[0].x - state.gate[1].x)
gate = draw_path(gate_start_x, gate_start_y, gate_path, grid_size, cell_size, state.gate_color)
# draw ball
ball_x = get_x_position(state.ball_pos.x, grid_size, cell_size)
ball_y = get_y_position(state.ball_pos.y, grid_size, cell_size)
ball = f'<circle cx="{ball_x}" cy="{ball_y}" r="10" fill="{state.ball_color}" />'
last_movement = state.solution_path.split(' ')[state.iteration]
last_movement_direction = get_direction(last_movement)
# draw robot
robot_pos_x = get_x_position(state.robot_pos.x, grid_size, cell_size)
robot_pos_y = get_y_position(state.robot_pos.y, grid_size, cell_size)
robot = f'<circle cx="{robot_pos_x}" cy="{robot_pos_y}" r="20" fill="{state.robot_color}" />'
robot_next_pos = calculate_next_position(state.robot_pos, last_movement_direction)
state.robot_pos = robot_next_pos # update robot position
if(state.robot_pos.x == state.ball_pos.x and state.robot_pos.y == state.ball_pos.y):
ball_next_pos = calculate_next_position(state.ball_pos, last_movement_direction)
state.ball_pos = ball_next_pos
# draw game board
return f'''
<svg id='patternId' width='{width}' height='{height}' xmlns='http://www.w3.org/2000/svg'>
<defs><pattern id='a' patternUnits='userSpaceOnUse' width='{cell_size}' height='{cell_size}' patternTransform='rotate(0)'>
<rect x='0' y='0' width='100%' height='100%' fill='hsla(0,0%,100%,1)'/>
<path d='M 0,0 V {cell_size} Z M 0,0 H {cell_size} Z' stroke-width='1' stroke='hsla(133, 35%, 32%, 1)' fill='none'/>
</pattern></defs>
<rect width='100%' height='100%' fill='url(#a)'/>
{path}
{gate}
{ball}
{robot}
</svg>
'''
def get_x_position(pos, grid_size, cell_size):
return pos * cell_size
def get_y_position(pos, grid_size, cell_size):
return (grid_size - pos) * cell_size
def draw_path(x, y, path, grid_size, cell_size, color):
lines = []
next_x = x
next_y = y
for dir in path:
direction: Direction = get_direction(dir)
lines.append(generate_line(next_x, next_y, direction, 1, grid_size, cell_size, color))
next_x, next_y = calculate_next_position_xy(next_x, next_y, direction)
return str.join(" ", lines)
def calculate_next_position(pos: Position, direction: Direction):
(next_x, next_y) = calculate_next_position_xy(pos.x, pos.y, direction)
return Position(next_x, next_y)
def calculate_next_position_xy(x, y, direction: Direction):
match direction:
case Direction.Down:
return (x, y - 1)
case Direction.Up:
return (x, y + 1)
case Direction.Left:
return (x - 1, y)
case Direction.Right:
return (x + 1, y)
def get_direction(dir):
match dir:
case 'D':
return Direction.Down
case 'U':
return Direction.Up
case 'L':
return Direction.Left
case 'R':
return Direction.Right
def generate_line(x, y, direction: Direction, length, grid_size, cell_size, color):
target_x = x
target_y = (grid_size) - y
match direction:
case Direction.Down:
target_y = target_y + length # SVG grid is top to bottom
case Direction.Up:
target_y = target_y - length # SVG grid is top to bottom
case Direction.Right:
target_x = target_x + length
case Direction.Left:
target_x = target_x - length
scaled_x = x * cell_size
scaled_y = ((grid_size) - y) * cell_size
scaled_target_x = target_x * cell_size
scaled_target_y = target_y * cell_size
return f'''
<path d='M {scaled_x} {scaled_y} L {scaled_target_x} {scaled_target_y}' stroke-width='3' stroke='{color}'/>
'''
def next_frame(body: Html, state: State):
body.set_content(build_svg(state))
state.iteration = state.iteration + 1
def toggle_timer(timer: Timer):
if(timer.active):
timer.deactivate()
else:
timer.activate()
def reset(state: State):
state.ball_pos = Position(2, 1)
state.robot_pos = Position(7, 4)
state.grid_size = 10
state.cell_size = 50
state.iteration = 0
state.solution_path = 'D D D D L L L L L U U U U U U L U R R R R R R R R R'
state.start_pos = state.robot_pos
state.gate = (Position(10, 5), Position(10, 8))
state.ball_color = 'red'
state.robot_color = 'green'
state.path_color = 'orange'
state.gate_color = 'blue'
interval = 1.0 # seconds
timer = ui.timer(interval, lambda: next_frame(body, state))
timer.deactivate()
state = State()
with ui.row():
ui.button('Play / Pause', on_click= lambda: toggle_timer(timer))
ui.button('Reset', on_click= lambda: reset(state))
body = ui.html().classes('self-center')
reset(state)
ui.run()