Skip to content

Instantly share code, notes, and snippets.

@mafshin
Created December 25, 2023 11:18
Show Gist options
  • Save mafshin/fb4d0ea8aefa806a24d0d5110303a265 to your computer and use it in GitHub Desktop.
Save mafshin/fb4d0ea8aefa806a24d0d5110303a265 to your computer and use it in GitHub Desktop.
Interactive Solution Drawing of Problem 40 with NiceGUI
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment