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
--
http://mail.python.org/mailman/listinfo/python-list

Reply via email to