#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Tkinter adaptation of CoolWorld lib.
Implements:
class TkWorld2D -- Tkinter adaptation of world.World2D class.
class TkWorld3D -- Tkinter adaptation of world.World3D class.
class TkWorldCanvas2D -- World2D adaptation of tkinter.Canvas
class.
class TkWorldCanvas3D -- World3D adaptation of tkinter.Canvas
class.
class TkWorldEventManager -- Tkinter adaptation of
events.WorldEventManager class.
class TkWorldImageManager -- Tkinter adaptation of
images.WorldImageManager class.
class TkWorldPlayer2D -- Tkinter adaptation of visual.WorldPlayer2D
class.
class TkWorldPlayer3D -- Tkinter adaptation of visual.WorldPlayer3D
class.
class TkWorldShapeBase2D -- base class for 2D shape management in a
tkinter environment.
class TkWorldShapeBase3D -- base class for 3D shape management in a
tkinter environment.
class TkWorldShapeCircle -- Tkinter adaptation of
shapes.WorldShapeCircle2D class.
class TkWorldShapeImage -- Tkinter adaptation of
shapes.WorldShapeImage2D class.
class TkWorldShapeOval -- Tkinter specific implementation for 2D
oval shapes.
class TkWorldShapePolygon -- Tkinter adaptation of
shapes.WorldShapePolygon2D class.
class TkWorldShapeRectangle -- Tkinter adaptation of
shapes.WorldShapeRectangle2D class.
class TkWorldSoundManager -- dummy class (Tkinter does not manage
sounds at all).
class TkWorldSprite2D -- Tkinter adaptation of visual.WorldSprite2D
class.
class TkWorldSprite3D -- Tkinter adaptation of visual.WorldSprite3D
class.
class TkWorldView -- Tkinter adaptation of visual.WorldView class.
"""
"""
The MIT License (MIT)
Copyright (c) 2015 Raphaël SEBAN
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import tkinter as tk
try:
from . import game
except:
import game
# end try
[docs]class TkWorld2D (game.World2D):
"""Tkinter adaptation of world.World2D class."""
def __init__ (self, tk_parent, **options):
"""Class constructor.
Parameters:
tk_parent -- tkinter parent widget.
options -- keyword arguments.
Supported keyword arguments:
name -- object's arbitrary name (default: class name).
area -- world's playground area (default:
world.World2D.DEFAULT_AREA).
"""
super().__init__(**options)
self.tk_parent = tk_parent
self._loop_tid = 0
# end def
[docs] def end (self, *args, **kw):
"""Try to break world's game loop.
Override or extend this method in subclasses whenever you need
more precise behaviour on game end request.
"""
self._running = False
self.tk_parent.after_cancel(self._loop_tid)
self.tk_parent.quit()
# end def
[docs] def game_loop (self, fps, sequence):
"""Game's generic main loop.
Parameters:
fps -- required number of frames per second (steps/second).
sequence -- sorted sequence of world item names to manage.
Feel free to override or extend this method to best meet your
own needs.
"""
# hook method to reimplement in subclass
_delay = self.fps_to_delay(fps)
self.game_loop_after(_delay, sequence)
self.tk_parent.mainloop()
# end def
[docs] def game_loop_after (self, delay, sequence):
"""Actual game loop launcher (with time delay).
Parameters:
delay -- required delay of time between each game step.
Current implementation expects delay in milliseconds.
sequence -- sequential list of world item names to manage.
This method allows deferred calls to the actual game loop for
many reasons:
- a deferred call allows to enter tkinter's mainloop() main
event management loop;
- it avoids multiple deferred calls to a same callable;
- it synchronizes game loop in time, according to required FPS
value.
"""
self.tk_parent.after_cancel(self._loop_tid)
if self._running:
self._loop_tid = self.tk_parent.after(
int(delay), self.internal_loop, delay, sequence
)
# end if
# end def
[docs] def internal_loop (self, delay, sequence):
"""Tkinter world's actual game loop.
If you really need to change something, it is probably here.
"""
_dt = delay/1000.0
for _item_name in sequence:
self.get(_item_name).do_threaded_step(dt=_dt)
# end for
self.game_loop_after(delay, sequence)
# end def
# end class
[docs]class TkWorld3D (game.World3D, TkWorld2D):
"""Tkinter adaptation of world.World3D class."""
def __init__ (self, tk_parent, **options):
"""Class constructor.
Parameters:
tk_parent -- tkinter parent widget.
options -- keyword arguments.
Supported keyword arguments:
name -- object's arbitrary name (default: class name).
area -- world's playground area (default:
world.World3D.DEFAULT_AREA).
"""
TkWorld2D.__init__(self, tk_parent, **options)
# end def
# end class
[docs]class TkWorldCanvas2D (tk.Canvas):
"""World2D adaptation of tkinter.Canvas class."""
DEFAULT_UNIT_VECTOR = (1, -1)
def __init__ (self, master=None, **options):
"""Class constructor.
Parameters:
master -- tkinter parent widget (default: None).
options -- keyword arguments.
Supported keyword arguments:
unit_vector -- user-specific unit vector.
tkinter options -- see tkinter.Canvas doc for more detail.
Notice: tkinter.Canvas object uses a special unit vector, as
point of origin is located at top left corner (head-down
oriented Y axis).
"""
self.unit_vector = options.pop("unit_vector", None)
super().__init__(master, **options)
# end def
@property
def unit_vector (self):
"""Geometrical unit vector - read-write attribute.
Helper for local coordinates calculations.
"""
return self.__unit_vector
# end def
@unit_vector.setter
[docs] def unit_vector (self, value):
self.__unit_vector = self.vector_type(
value or self.DEFAULT_UNIT_VECTOR
)
# end def
@property
[docs] def vector_type (self):
"""Preferred vector type - read-only attribute.
Current implementation returns geometry.WorldVector2D class
type.
This can be redefined in subclasses to fit more specific needs.
"""
return game.WorldVector2D
# end def
# end class
[docs]class TkWorldCanvas3D (TkWorldCanvas2D):
"""World3D adaptation of tkinter.Canvas class."""
DEFAULT_UNIT_VECTOR = (1, -1, 1)
@property
[docs] def vector_type (self):
"""Preferred vector type - read-only attribute.
Current implementation returns geometry.WorldVector3D class
type.
This can be redefined in subclasses to fit more specific needs.
"""
return game.WorldVector3D
# end def
# end class
[docs]class TkWorldEventManager (game.WorldEventManager):
"""Tkinter adaptation of events.WorldEventManager class.
Current implementation does nothing.
Please, refer to events.WorldEventManager implementation for more
details.
"""
pass # please, refer to events.WorldEventManager implementation
# end class
[docs]class TkWorldImageManager (game.WorldImageManager):
"""Tkinter adaptation of images.WorldImageManager class.
Please, refer to images.WorldImageManager implementation for more
details.
"""
[docs] def image_factory (self, filepath):
"""Produce and return an image object.
Parameter:
filepath -- path to image resource file.
"""
return tk.PhotoImage(file=filepath)
# end def
# end class
[docs]class TkWorldShapeBase2D:
"""Base class for 2D shape management in a tkinter environment."""
DEFAULT_UNIT_VECTOR = (1, -1)
def __init__ (self, canvas):
"""Class constructor.
Parameter:
canvas -- tkinter parent canvas widget.
Notice: a special unit vector has to be used, as tkinter.Canvas
widget locates its point of origin at the top left corner i.e.
head-down oriented Y axis.
"""
self.canvas = canvas
# overrides shapes.WorldAnchorage2D.unit_vector attribute
try:
self.unit_vector = canvas.unit_vector
except:
self.unit_vector = self.DEFAULT_UNIT_VECTOR
# end try
# end def
[docs] def finalize (self, *args, **kw):
"""Finalize shape object.
Here, shape object is merely deleted from parent canvas widget.
"""
self.canvas.delete(self.canvas_id)
# end def
[docs] def hide (self, *args, **kw):
"""Hide shape object."""
self.canvas.itemconfigure(self.canvas_id, state=tk.HIDDEN)
# end def
[docs] def initialize (self, **tk_options):
"""Initialize shape object.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
"""
_tkoptions = dict(state=tk.HIDDEN)
_tkoptions.update(self.options.get("tk_options") or {})
_tkoptions.update(tk_options)
self.canvas_id = self.shape_factory(**_tkoptions)
# end def
[docs] def lower_below (self, tag_or_id):
"""Place shape object layer under given tag or id object layer.
Parameter:
tag_or_id -- canvas item's tag or id. Can also be the
constant tkinter.ALL.
"""
self.canvas.tag_lower(self.canvas_id, tag_or_id)
# end def
[docs] def raise_above (self, tag_or_id):
"""Place shape object layer over given tag or id object layer.
Parameter:
tag_or_id -- canvas item's tag or id. Can also be the
constant tkinter.ALL.
"""
self.canvas.tag_raise(self.canvas_id, tag_or_id)
# end def
[docs] def render (self, new_position=None, **tk_options):
"""Render shape object in parent canvas.
Parameters:
new_position -- tuple coordinates (x, y) of anchorage
origin point's new position in parent canvas.
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
"""
if new_position:
self.origin = new_position
# end if
_tkoptions = dict(state=tk.NORMAL)
_tkoptions.update(tk_options)
self.canvas.coords(self.canvas_id, *self.shape_coords())
self.canvas.itemconfigure(self.canvas_id, **_tkoptions)
# end def
[docs] def shape_coords (self, *args, **kw):
"""Shape object list of coordinates - hook method.
Should return some tkinter-compatible list of coordinates.
"""
# hook method to reimplement in subclass
return self.bbox.to_flat_tuple()
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
This hook method MUST be overridden in subclass or it will
raise a NotImplementedError.
Creates shape on parent canvas along with its own specific
options.
"""
# hook method to reimplement in subclass
raise NotImplementedError
# end def
[docs] def show (self, *args, **kw):
"""Make shape object visible onto parent canvas."""
self.canvas.itemconfigure(self.canvas_id, state=tk.NORMAL)
# end def
# end class
[docs]class TkWorldShapeBase3D (TkWorldShapeBase2D):
"""Base class for 3D shape management in a tkinter environment."""
DEFAULT_UNIT_VECTOR = (1, -1, 1)
# end class
[docs]class TkWorldShapeCircle (TkWorldShapeBase2D, game.WorldShapeCircle2D):
"""Tkinter adaptation of shapes.WorldShapeCircle2D class."""
def __init__ (self, canvas, origin, radius, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
origin -- tuple coordinates (x, y) or point object for
circle's anchorage point of origin.
radius -- numeric value for circle's radius.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query - see geometry.WorldAnchorage2D.
unit_vector -- unit vector - see geometry.WorldAnchorage2D.
Notice: options are saved into self.options member attribute.
"""
TkWorldShapeBase2D.__init__(self, canvas)
game.WorldShapeCircle2D.__init__(
self, origin, radius, **options
)
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
Creates shape on parent canvas along with its own specific
options.
"""
return self.canvas.create_oval(0, 0, 0, 0, **tk_options)
# end def
# end class
[docs]class TkWorldShapeImage (TkWorldShapeBase2D, game.WorldShapeImage2D):
"""Tkinter adaptation of shapes.WorldShapeImage2D class."""
def __init__ (self, canvas, origin, image, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
origin -- tuple coordinates (x, y) or point object for
image's anchorage point of origin.
image -- image object.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query - see geometry.WorldAnchorage2D.
unit_vector -- unit vector - see geometry.WorldAnchorage2D.
Notice: options are saved into self.options member attribute.
"""
TkWorldShapeBase2D.__init__(self, canvas)
game.WorldShapeImage2D.__init__(
self, origin, image, **options
)
# end def
[docs] def shape_coords (self, *args, **kw):
"""Shape object list of coordinates - hook method.
Returns here self.center_point attribute.
"""
return self.center_point
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
Creates shape on parent canvas along with its own specific
options.
"""
return self.canvas.create_image(
0, 0, image=self.image, **tk_options
)
# end def
# end class
[docs]class TkWorldShapeOval (TkWorldShapeBase2D,
game.WorldShapeRectangle2D):
"""Tkinter specific implementation for 2D oval shapes."""
def __init__ (self, canvas, origin, width, height, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
origin -- tuple coordinates (x, y) or point object for
oval's anchorage point of origin.
width -- oval bounding box' width.
height -- oval bounding box' height.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query - see geometry.WorldAnchorage2D.
unit_vector -- unit vector - see geometry.WorldAnchorage2D.
Notice: options are saved into self.options member attribute.
"""
TkWorldShapeBase2D.__init__(self, canvas)
game.WorldShapeRectangle2D.__init__(
self, origin, width, height, **options
)
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
Creates shape on parent canvas along with its own specific
options.
"""
return self.canvas.create_oval(0, 0, 0, 0, **tk_options)
# end def
# end class
[docs]class TkWorldShapePolygon (TkWorldShapeBase2D,
game.WorldShapePolygon2D):
"""Tkinter adaptation of shapes.WorldShapePolygon2D class."""
def __init__ (self, canvas, origin, *coords, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
origin -- tuple coordinates (x, y) or point object for
polygon's anchorage point of origin.
coords -- variable list of tuple coordinates (x, y) or
point objects representing polygon's vertices.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query - see geometry.WorldAnchorage2D.
unit_vector -- unit vector - see geometry.WorldAnchorage2D.
Notice: options are saved into self.options member attribute.
"""
TkWorldShapeBase2D.__init__(self, canvas)
game.WorldShapePolygon2D.__init__(
self, origin, *coords, **options
)
# end def
[docs] def shape_coords (self, *args, **kw):
"""Shape object list of coordinates - hook method.
Returns here self.rel_coords_flat attribute.
"""
return self.rel_coords_flat
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
Creates shape on parent canvas along with its own specific
options.
"""
return self.canvas.create_polygon(0, 0, **tk_options)
# end def
# end class
[docs]class TkWorldShapeRectangle (TkWorldShapeBase2D,
game.WorldShapeRectangle2D):
"""Tkinter adaptation of shapes.WorldShapeRectangle2D class."""
def __init__ (self, canvas, origin, width, height, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
origin -- tuple coordinates (x, y) or point object for
rectangle's anchorage point of origin.
width -- rectangle's width.
height -- rectangle's height.
options -- keyword arguments.
Supported keyword arguments:
anchor -- anchorage query - see geometry.WorldAnchorage2D.
unit_vector -- unit vector - see geometry.WorldAnchorage2D.
Notice: options are saved into self.options member attribute.
"""
TkWorldShapeBase2D.__init__(self, canvas)
game.WorldShapeRectangle2D.__init__(
self, origin, width, height, **options
)
# end def
[docs] def shape_factory (self, **tk_options):
"""Shape specific builder - hook method.
Parameter:
tk_options -- shape's tkinter-specific options.
Please, refer to Tkinter documentation for more detail.
Creates shape on parent canvas along with its own specific
options.
"""
return self.canvas.create_rectangle(0, 0, 0, 0, **tk_options)
# end def
# end class
[docs]class TkWorldSoundManager:
"""dummy class (Tkinter does not manage sounds at all)."""
# Caution: tkinter does not manage sounds
pass
# end class
[docs]class TkWorldSprite2D (game.WorldSprite2D):
"""Tkinter adaptation of visual.WorldSprite2D class."""
DEFAULT_UNIT_VECTOR = (1, -1)
# end class
[docs]class TkWorldSprite3D (game.WorldSprite3D):
"""Tkinter adaptation of visual.WorldSprite3D class."""
DEFAULT_UNIT_VECTOR = (1, -1, 1)
# end class
[docs]class TkWorldPlayer2D (TkWorldSprite2D, game.WorldPlayer2D):
"""Tkinter adaptation of visual.WorldPlayer2D class.
Current implementation does nothing.
Please, refer to visual.WorldPlayer2D implementation for more
detail.
"""
pass # please, refer to visual.WorldPlayer2D implementation
# end class
[docs]class TkWorldPlayer3D (TkWorldSprite3D, game.WorldPlayer3D):
"""Tkinter adaptation of visual.WorldPlayer3D class.
Current implementation does nothing.
Please, refer to visual.WorldPlayer3D implementation for more
detail.
"""
pass # please, refer to visual.WorldPlayer3D implementation
# end class
[docs]class TkWorldView (game.WorldView):
"""Tkinter adaptation of visual.WorldView class."""
def __init__ (self, canvas, world, name=None, **options):
"""Class constructor.
Parameters:
canvas -- tkinter parent canvas widget.
world -- parent world containing view world item.
name -- world item's arbitrary name (default: None). If
None or omitted, will be replaced by current class name.
options -- keyword arguments.
Supported keyword arguments:
-- none at this stage --
Notice: options are saved into self.options member attribute.
"""
super().__init__(world, name, **options)
self.canvas = canvas
# end def
# end class
[docs]def run_demo ():
class Player (TkWorldPlayer2D):
def action_move_down (self, *args, **kw):
self.position = self.next_step(offset=(0, -1))
# end def
def action_move_up (self, *args, **kw):
self.position = self.next_step(offset=(0, 1))
# end def
def do_step (self, *args, **kw):
pass
# end def
def finalize (self):
self.shape.finalize()
# end def
def initialize (self):
self.shape.initialize()
self.position = self.shape.origin
self.velocity = (0, 10)
# end def
def next_step (self, offset):
_new_position = (
self.position + self.velocity.cdot(
self.unit_vector.cdot(offset)
)
)
self.shape.origin = _new_position
if self.world.area.bounds.exited(*self.shape.bbox):
self.shape.origin = _new_position = self.position
# end if
return _new_position
# end def
# end class
class Ball (TkWorldPlayer2D):
def do_step (self, *args, **kw):
self.next_step()
# end def
def finalize (self):
self.shape.finalize()
# end def
def initialize (self):
self.shape.initialize()
self.position = self.shape.origin
self.velocity = (5, 5)
# end def
def next_step (self):
_new_position = (
self.position + self.velocity.cdot(self.unit_vector)
)
self.shape.origin = self.position = _new_position
_player = self.world.get("player")
if _player.shape.bbox.collided(*self.shape.bbox):
self.velocity.abs_x()
else:
_collisions = set(
self.world.area.bounds.exited(*self.shape.bbox)
)
if _collisions.intersection(("left", "right")):
self.velocity.flip_x()
# end if
if _collisions.intersection(("top", "bottom")):
self.velocity.flip_y()
# end if
# end if
# end def
# end class
class View (TkWorldView):
def do_step (self, *args, **kw):
for _item in self.world.get_items(exclude=self):
try:
_item.shape.render()
except Exception as error:
print("caught exception:", error)
# end try
# end for
# end def
# end class
_root = tk.Tk()
_root.title("Tkinter game demo")
_root.resizable(0, 0)
_canvas = TkWorldCanvas2D(_root, background="black")
_world = TkWorld2D(_root, area=(320, 240))
_player = Player(_world)
_player.shape = TkWorldShapeRectangle(
_canvas, origin=(10, _world.area.center_y), width=10,
height=30, tk_options=dict(fill="white"),
)
_ball = Ball(_world)
_ball.shape = TkWorldShapeRectangle(
_canvas, origin=_world.area.center_xy, width=10, height=10,
tk_options=dict(fill="white"),
)
_view = View(_canvas, _world)
_root.bind("<Up>", _player.action_move_up)
_root.bind("<Down>", _player.action_move_down)
_root.bind("<Escape>", _world.end)
_canvas.configure(**_world.area.to_dict())
_canvas.pack()
_world.run()
# end def
if __name__ == "__main__":
run_demo()
# end if