Some silly code for Easter holiday
Alf P. Steinbach
alfps at start.no
Tue Mar 23 06:54:45 EDT 2010
This program simulates some colored balls moving around, changing color
according to certain rules. I think the most interesting is perhaps to not look
at this code but just try to run it and figure out the color changing rules from
observing the effect (extra mystery: why I wrote this). Sort of like an Easter
holiday mystery.
<code>
# Py3
# Copyright 2010 Alf P. Steinbach
import tkinter as tk
from collections import namedtuple
import random
Point = namedtuple( "Point", "x, y" )
Size = namedtuple( "Size", "x, y" )
RGB = namedtuple( "RGB", "r, g, b" )
def generator( g ):
assert isinstance( g, type( (i for i in ()) ) )
return g
def tk_internal_bbox_from( bbox: tuple ):
return ((bbox[0], bbox[1], bbox[2]+2, bbox[3]+2))
def tk_new_ellipse( canvas, bbox: tuple, **kwargs ):
return canvas.create_oval( tk_internal_bbox_from( bbox ), **kwargs )
class TkTimer:
def __init__( self, widget, msecs: int, action, start_running: bool = True ):
self._widget = widget
self._msecs = msecs
self._action = action
self._id = None
if start_running: self.start()
def start( self ):
self._id = self._widget.after( self._msecs, self._on_timer )
def stop( self ):
id = self._id;
self._id = None
self._widget.after_cancel( id ) # Try to cancel last event.
def _on_timer( self ):
if self._id is not None:
self._action()
self.start()
class TkEllipse:
def __init__( self, canvas, bbox: tuple, **kwargs ):
self._canvas = canvas
self._id = tk_new_ellipse( canvas, bbox, **kwargs )
@property # id
def id( self ): return self._id
@property # fill
def fill( self ):
return self._canvas.itemcget( self._id, "fill" )
@fill.setter
def fill( self, color_representation: str ):
self._canvas.itemconfigure( self._id,
fill = color_representation
)
@property # internal_bbox
def internal_bbox( self ):
return tuple( self._canvas.coords( self._id ) )
@property # position
def position( self ):
bbox = self.internal_bbox
return Point( bbox[0], bbox[1] )
@position.setter
def position( self, new_pos: Point ):
bbox = self.internal_bbox
(dx, dy) = (new_pos.x - bbox[0], new_pos.y - bbox[1])
self._canvas.move( self._id, dx, dy )
#assert self.position == new_pos
class Color:
def __init__( self, rgb_or_name ):
if isinstance( rgb_or_name, RGB ):
name = None
rgb = rgb_or_name
else:
assert isinstance( rgb_or_name, str )
name = rgb_or_name
rgb = None
self._name = name
self._rgb = rgb
@property
def representation( self ):
if self._name is not None:
return self._name
else:
rgb = self._rgb
return "#{:02X}{:02X}{:02X}".format( rgb.r, rgb.g, rgb.b )
def __str__( self ): return self.representation
def __hash__( self ): return hash( self.representation )
class Rectangle:
def __init__( self,
width : int,
height : int,
upper_left : Point = Point( 0, 0 )
):
self._left = upper_left.x
self._right = upper_left.x + width
self._top = upper_left.y
self._bottom = upper_left.y + height
@property # left
def left( self ): return self._left
@property # top
def top( self ): return self._top
@property # right
def right( self ): return self._right
@property # bottom
def bottom( self ): return self._bottom
@property # width
def width( self ): return self._right - self._left
@property # height
def height( self ): return self._bottom - self._top
@property # size
def size( self ): return Size( self.width, self.height )
class Ball:
def __init__( self,
color : Color,
position : Point = Point( 0, 0 ),
velocity : Point = Point( 0, 0 )
):
self.color = color
self.position = position
self.velocity = velocity
def squared_distance_to( self, other ):
p1 = self.position
p2 = other.position
return (p2.x - p1.x)**2 + (p2.y - p1.y)**2
class BallSim:
def __init__( self,
rect : Rectangle,
n_balls : int = 1
):
def random_pos():
return Point(
random.randrange( rect.left, rect.right ),
random.randrange( rect.top, rect.bottom )
)
def random_velocity():
return Point(
random.randint( -10, 10 ),
random.randint( -10, 10 )
)
def balls( color ):
return generator(
Ball( color, random_pos(), random_velocity() ) for i in range(
n_balls )
)
self._rect = rect
self._kind_1_color = Color( "blue" )
self._kind_2_color = Color( "orange" )
self._balls = tuple( balls( self._kind_1_color ) )
self._is_close_distance = 20;
@property # rect
def rect( self ): return self._rect
@property # interaction_radius
def interaction_radius( self ):
return self._is_close_distance
@property # n_balls
def n_balls( self ): return len( self._balls )
def ball( self, i ): return self._balls[i]
def balls( self ): return self._balls
def _update_positions_and_velocities( self ):
rect = self._rect
for ball in self._balls:
pos = ball.position; v = ball.velocity;
pos = Point( pos.x + v.x, pos.y + v.y )
if pos.x < 0:
pos = Point( -pos.x, pos.y )
v = Point( -v.x, v.y )
if pos.x >= rect.width:
pos = Point( 2*rect.width - pos.x, pos.y )
v = Point( -v.x, v.y )
if pos.y < 0:
pos = Point( pos.x, -pos.y )
v = Point( v.x, -v.y )
if pos.y >= rect.height:
pos = Point( pos.x, 2*rect.height - pos.y )
v = Point( v.x, -v.y )
ball.position = pos
ball.velocity = v
def _balls_possibly_close_to( self, ball ):
max_d_squared = self._is_close_distance**2
result = []
for other in self._balls:
if other is ball:
continue
if ball.squared_distance_to( other ) <= max_d_squared:
result.append( other )
return result
def _update_kinds( self ):
max_d_squared = self._is_close_distance**2
for ball in self._balls:
if ball.color == self._kind_1_color:
for other_ball in self._balls_possibly_close_to( ball ):
if ball.squared_distance_to( other_ball ) <= max_d_squared:
if other_ball.color == self._kind_1_color:
ball.color = self._kind_2_color
other_ball.color = self._kind_2_color
break
else:
if random.random() < 0.01:
ball.color = self._kind_1_color
def evolve( self ):
self._update_positions_and_velocities()
self._update_kinds()
class View:
def _create_widgets( self, parent_widget, sim: BallSim ):
self.widget = tk.Frame( parent_widget )
if True:
canvas = tk.Canvas(
self.widget, bg = "white", width = sim.rect.width, height =
sim.rect.height
)
canvas.pack()
self._canvas = canvas
self._circles = []
radius = sim.interaction_radius // 2
self._ball_radius = radius
for ball in sim.balls():
(x, y) = (ball.position.x, ball.position.y)
bbox = (x - radius, y - radius, x + radius, y + radius)
ellipse = TkEllipse( canvas, bbox, fill =
ball.color.representation )
self._circles.append( ellipse )
pass
def __init__( self, parent_widget, sim: BallSim ):
self._create_widgets( parent_widget, sim )
self._sim = sim
def update( self ):
sim = self._sim
r = self._ball_radius
for (i, ball) in enumerate( sim.balls() ):
center_pos = ball.position
self._circles[i].position = Point( center_pos.x - r, center_pos.y - r )
self._circles[i].fill = ball.color
class Controller:
def __init__( self, main_window ):
self._window = main_window
self._model = BallSim( Rectangle( 600, 500 ), n_balls = 20 )
self._view = view = View( main_window, self._model )
view.widget.place( relx = 0.5, rely = 0.5, anchor="center" )
self._timer = TkTimer( main_window, msecs = 42, action = self._on_timer )
def _on_timer( self ):
self._model.evolve()
self._view.update()
def main():
window = tk.Tk()
window.title( "Sim 1 -- Chameleon Balls" )
window.geometry( "640x510" )
controller = Controller( window )
window.mainloop()
main()
</code>
Cheers,
- Alf
More information about the Python-list
mailing list