Some silly code for Easter holiday

Alf P. Steinbach alfps at start.no
Tue Mar 23 11:54:45 CET 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