Some silly code for Easter holiday

A

Alf P. Steinbach

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
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.position = Point( center_pos.x - r, center_pos.y - r )
self._circles.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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,535
Members
45,007
Latest member
obedient dusk

Latest Threads

Top