Цель игры - провести игрового персонажа (зайца) как можно дальше через бесконечное поле, полное железнодорожных путей. Путь для героя осложняют многочисленные кустарники, преграждающие путь и скоростные поезда, которые могут переехать героя.

Основные текстуры, используемые в игре нужно скачать с github оригинальной игры: https://github.com/Wireframe-Magazine/Code-the-Classics/tree/master/bunner-master/images

from tkinter import *
from random import randint, shuffle, choice
from time import time
from tkinter import messagebox

root = Tk()
 
canvas = Canvas(root, bg="yellow",
           height=560, width=480)

imageLib = {
    "sit-left": PhotoImage(file=f"textures/sit3.png"),
    "sit-right": PhotoImage(file=f"textures/sit1.png"),
    "sit-up": PhotoImage(file=f"textures/sit0.png"),
    "sit-down": PhotoImage(file=f"textures/sit2.png"),
    "rail1": PhotoImage(file=f"textures/rail1.png"),
    "rail2": PhotoImage(file=f"textures/rail2.png"),
    "rail3": PhotoImage(file=f"textures/rail3.png"),
    "train-yellow-l": PhotoImage(file=f"textures/train00.png"),
    "train-orange-r": PhotoImage(file=f"textures/train11.png"),
    "bush": PhotoImage(file=f"textures/bush31.png"),
    "dead": PhotoImage(file=f"textures/splat0.png"),
}

for i in range(7, 15):
    imageLib[f"grass{i}"] = PhotoImage(file=f"textures/grass{i}.png")

class GrassRow:
    image = False
    element = False
    train = False
    bushes = False

    y = -1

    def __init__(self, x, y, img="random", bushed=True):
        self.y = y
        self.image = imageLib[f"grass{randint(7, 14)}"] if img=="random" else imageLib[img]
        self.element = canvas.create_image((x, y), image=self.image)

        if bushed: self.bushes = [True, True, True, False, False, False, False, False, False, False, False, False]
        else: 
            self.bushes = [False]*12

        shuffle(self.bushes)
        # print(self.bushes)
        for i in range(12):
            if self.bushes[i]:
                self.bushes[i] = canvas.create_image((i*40+20, self.getY()), image=imageLib["bush"])
        # for i in range(50):
        #     self.trainArrives.append(time()+i*10)

    def update(self, dx, dy):
        self.y+=dy
        canvas.move(self.element, dx, dy)
        for i in self.bushes:
            if i == False: continue
            canvas.move(i, dx, dy)

    def getY(self):
        # return canvas.coords(self.element)[1]
        return self.y
    
trainSpd = 30

worldY = 0

class RailsRow:
    image = imageLib["rail2"]
    trainImage = imageLib["train-yellow-l"]
    element = False
    train = False
    trainDir = 1
    bushes = [False]*12
    

    y=-1

    def __init__(self, x, y):
        self.y = y
        # self.image = imageLib[f"grass{randint(7, 14)}"]
        self.element = canvas.create_image((x, y), image=self.image)
        self.trainArrives = time()+randint(2, 5)

    def getTrainBounds(self):
        halfTrain = self.trainImage.width()/2
        tx = canvas.coords(self.train)[0]
        ty = self.getY()
        return (tx-halfTrain, ty, tx+halfTrain, ty+5)

    def update(self, dx, dy):
        canvas.move(self.element, dx, dy)
        self.y+=dy
        if 0 < (time()-self.trainArrives):
            canvas.tkraise(self.train)
            if self.train== False:
                self.trainDir = choice((-1, 1))
                self.trainImage = imageLib["train-yellow-l" if self.trainDir == -1 else "train-orange-r"]
                self.train = canvas.create_image((480+self.trainImage.width()/2 if self.trainDir == -1 else -self.trainImage.width()/2, self.getY()-20), image=self.trainImage)
            else:
                # print(trainSpd*self.trainDir, self.trainDir)
                canvas.move(self.train, trainSpd*self.trainDir, dy)
                if rabbit.element in canvas.find_overlapping(*self.getTrainBounds()): 
                    rabbit.image = imageLib["dead"]
                    rabbit.changeImage()
                    messagebox.showerror(message="Заяц сдох!")
                    exit()
                # print("movce")
                if (self.trainDir == -1 and canvas.coords(self.train)[0] < -self.trainImage.width()/2) or (self.trainDir == 1 and canvas.coords(self.train)[0] > 480 + self.trainImage.width()/2):
                    canvas.delete(self.train)
                    self.train = False
                    self.trainArrives=time()+randint(2, 5)



    def getY(self):
        # return canvas.coords(self.element)[1]
        return self.y

# for i in range(0, 580, 40):
#     GrassRow(240, i)

worldSpeed = 1

class Rabbit:
    image = imageLib["sit-up"]

    myRow = 4

    element = False

    posX = 6

    def __init__(self):
        self.element = canvas.create_image((260, 560-20-160), image=self.image)
        canvas.tkraise(self.element)
        

    def changeImage(self):
        canvas.itemconfigure(self.element, image=self.image)

    def getCellX(self):
        # print(int(canvas.coords(self.element)[0]//40), canvas.coords(self.element)[0])
        # return int(canvas.coords(self.element)[0]//40)
        print(self.posX, self.myRow)
        return self.posX

    def update(self, dx, dy):
        
        canvas.tkraise(self.element)
        if dx>=40: 
            self.image = imageLib["sit-right"]
            if area[self.myRow].bushes[self.getCellX()+1] != False: return
            self.posX += 1
        if dx<=-40: 
            self.image = imageLib["sit-left"]
            if area[self.myRow].bushes[self.getCellX()-1] != False: return
            self.posX -= 1
        if dy>=40: 
            self.image = imageLib["sit-down"]
            if area[self.myRow-1].bushes[self.getCellX()] != False: return
            self.myRow -= 1
        if dy<=-40: 
            self.image = imageLib["sit-up"]
            if area[self.myRow+1].bushes[self.getCellX()] != False: return
            self.myRow += 1

        
        # if abs(dy) > worldSpeed: print(dy)
        self.changeImage()
        canvas.move(self.element, dx, dy)



root.bind("<Up>", lambda _: rabbit.update(0, -40))
root.bind("<Down>", lambda _: rabbit.update(0, 40))
root.bind("<Left>", lambda _: rabbit.update(-40, 0))
root.bind("<Right>", lambda _: rabbit.update(40, 0))



area = [GrassRow(240, 560-20, bushed=False)]

def getNextAreaY():
    return area[-1].getY()-40

rabbit = Rabbit()

while len(area) < 20:
    type = choice(["rail", "grass"])
    if type == "rail":
        area.append(GrassRow(240, getNextAreaY(), "rail1"))
        area.append(RailsRow(240, getNextAreaY()))
        area.append(GrassRow(240, getNextAreaY(), "rail3"))
    if type == "grass":
        for i in range(randint(0, 3)):
            area.append(GrassRow(240, getNextAreaY()))

def updateAll():
    if canvas.coords(rabbit.element)[1] < 400: worldSpeed = 400/canvas.coords(rabbit.element)[1]
    else: worldSpeed = 1
    rabbit.update(0, worldSpeed)
    removalList = []
    for ID, i in enumerate(area):
        
        i.update(0, worldSpeed)
        if i.getY() > 640:
            removalList.append(ID)

    for ID, i in enumerate(removalList):
        if area[i].train: canvas.delete(area[i].train)
        canvas.delete(area[i].element)
        
        area.pop(i-ID)
        print("pop", i-ID, len(area))
        rabbit.myRow -= 1

    # print(len(area))

    while len(area) < 30:
        type = choice(["rail", "grass"])
        if type == "rail":
            area.append(GrassRow(240, getNextAreaY(), "rail1"))
            area.append(RailsRow(240, getNextAreaY()))
            area.append(GrassRow(240, getNextAreaY(), "rail3"))
        if type == "grass":
            for i in range(randint(0, 3)):
                area.append(GrassRow(240, getNextAreaY()))
        
    root.after(20, updateAll)

updateAll()

canvas.pack()
mainloop()