Code: Alles auswählen
#!/usr/bin/env python3
from functools import partial, total_ordering
from random import randrange
from tkinter import ARC, NE, NW, Canvas, IntVar, Tk, font, messagebox
DIFFICULTY_FACTOR = 0.95
EGG_WIDTH = 45
EGG_HEIGHT = 55
EGG_MOVE_DELAY = 500
EGG_DROP_INTERVAL = 4000
BASKET_COLOR = "blue"
BASKET_WIDTH = BASKET_HEIGHT = 100
class Box:
def __init__(self, left, top, right, bottom):
self._left = left
self._top = top
self._right = right
self._bottom = bottom
def __repr__(self):
return (
f"{self.__class__.__name__}"
f"{self.left, self.top, self.right, self.bottom}"
)
def __iter__(self):
yield self.left
yield self.top
yield self.right
yield self.bottom
@property
def left(self):
return self._left
@left.setter
def left(self, value):
self.move(value - self.left, 0)
@property
def top(self):
return self._top
@top.setter
def top(self, value):
self.move(0, value - self.top)
@property
def right(self):
return self._right
@right.setter
def right(self, value):
self.move(value - self.right, 0)
@property
def bottom(self):
return self._bottom
@bottom.setter
def bottom(self, value):
self.move(0, value - self.bottom)
@property
def width(self):
return self.right - self.left
@property
def height(self):
return self.bottom - self.top
@property
def center_x(self):
return self.width // 2 + self.left
@center_x.setter
def center_x(self, value):
self.move(value - self.center_x, 0)
def move(self, x_delta, y_delta):
self._left += x_delta
self._top += y_delta
self._right += x_delta
self._bottom += y_delta
@classmethod
def from_position_and_size(cls, x, y, width, height):
return cls(x, y, x + width, y + height)
@classmethod
def from_size(cls, width, height):
return cls.from_position_and_size(0, 0, width, height)
class Display(Canvas):
def __init__(self, master, width, height):
self.width = width
self.height = height
self._box = Box.from_size(self.width, self.height)
Canvas.__init__(
self,
master=master,
width=self.width,
height=self.height,
background="deep sky blue",
)
#
# Ground and sun.
#
# TODO Let sizes depend on display size.
#
self.create_rectangle(
-5,
self.height - 300,
self.width + 5,
self.height + 5,
fill="sea green",
width=0,
)
#
# TODO `Box.from_center_and_radius()`!?
#
self.create_oval(-80, -80, 120, 120, fill="orange", width=0)
game_font = font.nametofont("TkFixedFont")
game_font.config(size=18)
self._score_text_id = self.create_text(
10, 10, anchor=NW, font=game_font, fill="darkblue",
)
self._life_count_text_id = self.create_text(
self.width - 10, 10, anchor=NE, font=game_font, fill="darkblue",
)
self.set_score("-")
self.set_life_count("-")
def get_box(self):
return self._box
def set_score(self, value):
self.itemconfigure(self._score_text_id, text=f"Punktzahl: {value}")
def set_life_count(self, value):
self.itemconfigure(self._life_count_text_id, text=f"Leben {value}")
@total_ordering
class Sprite:
def __init__(self, display, item_id):
self.display = display
self.id = item_id
def __str__(self):
return str(self.id)
def __eq__(self, other):
return self.id == other.id
def __ne__(self, other):
return not self == other
def __lt__(self, other):
return self.id < other.id
def __hash__(self):
return hash(self.id)
def get_box(self):
return Box(*self.display.coords(self))
def move(self, x_delta, y_delta):
self.display.move(self, x_delta, y_delta)
def delete(self):
self.display.delete(self)
self.id = None
@classmethod
def new_arc(cls, display, box, **kwargs):
return cls(display, display.create_arc(*box, **kwargs))
@classmethod
def new_oval(cls, display, box, **kwargs):
return cls(display, display.create_oval(*box, **kwargs))
def create_egg(display, eggs, egg_drop_interval_var):
eggs.append(
Sprite.new_oval(
display,
Box.from_position_and_size(
randrange(10, display.width - EGG_WIDTH - 10),
40,
EGG_WIDTH,
EGG_HEIGHT,
),
fill="yellow",
width=0,
)
)
display.after(
egg_drop_interval_var.get(),
create_egg,
display,
eggs,
egg_drop_interval_var,
)
def lose_a_life(display, life_count_var):
life_count_var.set(life_count_var.get() - 1)
display.set_life_count(life_count_var.get())
def on_uncatched_egg(display, eggs, score_var, life_count_var, egg):
eggs.remove(egg)
egg.delete()
lose_a_life(display, life_count_var)
if life_count_var.get() == 0:
#
# FIXME While the message box is shown there is still a task running
# creating new eggs.
#
messagebox.showinfo("Game Over", f"Punkte: {score_var.get()}")
display.quit()
def move_eggs(
display, score_var, life_count_var, eggs, egg_move_delay_var,
):
for egg in eggs:
egg.move(0, 10)
if egg.get_box().bottom > display.height:
on_uncatched_egg(display, eggs, score_var, life_count_var, egg)
display.after(
egg_move_delay_var.get(),
move_eggs,
display,
score_var,
life_count_var,
eggs,
egg_move_delay_var,
)
def increase_score(
display, score_var, egg_move_delay_var, egg_drop_interval_var
):
score_var.set(score_var.get() + 1)
#
# TODO Don't change the values of `egg_move_delay_var` and
# `egg_drop_interval_var` but calculate them from `score_var`.
#
egg_move_delay_var.set(int(egg_move_delay_var.get() * DIFFICULTY_FACTOR))
egg_drop_interval_var.set(
int(egg_drop_interval_var.get() * DIFFICULTY_FACTOR)
)
display.set_score(score_var.get())
def check_catch(
display,
score_var,
basket,
eggs,
egg_move_delay_var,
egg_drop_interval_var,
):
basket_box = basket.get_box()
for egg in eggs:
egg_box = egg.get_box()
if (
basket_box.left < egg_box.left
and basket_box.right > egg_box.right
and basket_box.bottom - egg_box.bottom < 40
):
eggs.remove(egg)
egg.delete()
increase_score(
display, score_var, egg_move_delay_var, egg_drop_interval_var
)
display.after(
100,
check_catch,
display,
score_var,
basket,
eggs,
egg_move_delay_var,
egg_drop_interval_var,
)
def move_basket_left(basket, _event):
if basket.get_box().left > 0:
basket.move(-20, 0)
def move_basket_right(display, basket, _event):
if basket.get_box().right < display.width:
basket.move(20, 0)
def main():
root = Tk()
display_width, display_height = (
root.winfo_screenwidth(),
root.winfo_screenheight(),
)
root.attributes("-fullscreen", True)
root.geometry(f"{display_width}x{display_height}+0+0")
egg_move_delay_var = IntVar(value=EGG_MOVE_DELAY)
egg_drop_interval_var = IntVar(value=EGG_DROP_INTERVAL)
score_var = IntVar(value=0)
life_count_var = IntVar(value=3)
eggs = list()
display = Display(root, display_width, display_height)
display.pack()
display.set_score(score_var.get())
display.set_life_count(life_count_var.get())
display_box = display.get_box()
basket_box = Box.from_size(BASKET_WIDTH, BASKET_HEIGHT)
basket_box.center_x = display_box.center_x
basket_box.bottom = display_box.bottom - 20
basket = Sprite.new_arc(
display,
basket_box,
start=200,
extent=140,
style=ARC,
outline=BASKET_COLOR,
width=3,
)
root.after(1000, create_egg, display, eggs, egg_drop_interval_var)
root.after(
1000,
move_eggs,
display,
score_var,
life_count_var,
eggs,
egg_move_delay_var,
)
#
# TODO This should not be an indepedent asynchronous task but called each
# time the eggs and/or the basket moved.
#
root.after(
1000,
check_catch,
display,
score_var,
basket,
eggs,
egg_move_delay_var,
egg_drop_interval_var,
)
display.bind("<Left>", partial(move_basket_left, basket))
display.bind("<Right>", partial(move_basket_right, display, basket))
display.focus_set()
root.mainloop()
if __name__ == "__main__":
main()
Als nächstes könnte man eine Klasse einführen, die die `eggs`-Liste durch ein Objekt ersetzt, welches sich um die Eier kümmert.