summaryrefslogtreecommitdiff
path: root/game.py
blob: 2724503da500c4684951cce92c19eb9966997727 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
from random import randint, random
from tkinter import Canvas, PhotoImage, Tk
from typing import List

from config import Config
from font import Font
from frame_counter import FrameCounter
from inputs import InputController
from sprite import Sprite
from textures import TextureFactory


class Game:
    """A generic game object"""

    def __init__(self) -> None:
        """Initialise the game
        """
        self.win = Tk()
        game_width, game_height = (
            Config.WIDTH*Config.SCALE, Config.HEIGHT*Config.SCALE
        )
        self.w, self.h = Config.WIDTH, Config.HEIGHT

        self.win.geometry(f"{game_width}x{game_height}")
        self.canvas = Canvas(self.win, width=game_width,
                             height=game_height, bg="#000")
        self.canvas.pack()

        self.texture_factory = TextureFactory(scale=Config.SCALE)
        self.effect_player = EffectPlayer(self)
        self.frame_counter = FrameCounter(self.canvas, Config.FPS)

        self.inputs = InputController(self)
        self.sprites = []

        self.score = 0

        self.alpha = 0

    def start(self):
        """Start the game"""
        self.loop()
        self.win.mainloop()

    def tick(self):
        """Update the game's sprites"""
        for sprite in self.sprites:
            sprite.tick()
        self.effect_player.tick()

    def loop(self):
        """Loop the game at a set framerate"""
        self.alpha += 1
        self.tick()
        self.frame_counter.next_frame(self.loop)

    def clear_all(self):
        """Remove all game sprites"""
        for sprite in self.sprites:
            sprite.destroy()
        self.sprites = []


class GameSprite(Sprite):
    """A sprite which belongs to a game"""

    def __init__(self, game: Game, image: PhotoImage):
        """Initialise the sprite

        :param game: The game which this belongs to
        :type game: Game
        :param image: The image to use for the sprite
        :type image: PhotoImage
        """
        self.game = game
        super().__init__(game.canvas, image, (0, 0))

    def move(self, x, y):
        """Move the sprite by a certain amount

        :param x: Amount of pixels to move right
        :param y: Amount of pixels to move down
        """
        # if the object needs to move less than a pixel
        # only move it every few frames to create this effect
        if abs(x) >= 1:
            self.x += x
        elif x != 0:
            if self.game.alpha % (1/x) == 0:
                self.x += 1

        if abs(y) >= 1:
            self.y += y
        elif y != 0:
            if self.game.alpha % (1/y) == 0:
                self.y += 1

        self.update_position()


class GameEffect(GameSprite):
    """An effect that can be played within game"""

    def __init__(self, game: Game, image: PhotoImage,
                 duration=10, momentum=(0, 0)):
        """Initialise the game effect

        :param game: The game which this belongs to
        :type game: Game
        :param image: The image to use for this
        :type image: PhotoImage
        :param duration: How long this effect should last for
        :param momentum: Which direction to move this effect
        """
        self.start_time = game.alpha
        self.duration = duration
        self.velocity_x, self.velocity_y = momentum
        super().__init__(game, image)

    def tick(self):
        """Move the effect by its momentum and remove it if its over"""
        super().tick()
        self.move(self.velocity_x, self.velocity_y)

        alpha = self.game.alpha - self.start_time
        if self.duration != -1 and alpha > self.duration:
            self.destroy()


class AnimatedEffect(GameEffect):
    """An effect which involves animating an image"""

    def __init__(self, game: Game, images: List[PhotoImage],
                 frame_time=1, momentum=(0, 0)):
        """Initialise the effect

        :param game: The game which this belongs to
        :type game: Game
        :param images: The images to use for this animation
        :type images: List[PhotoImage]
        :param frame_time: Length of each frame of the animation
        :param momentum: Direction to move this effect
        """
        self.start_time = game.alpha
        self.frame_time = frame_time
        self.images = images
        self.frame_start = game.alpha
        super().__init__(game, images[0], duration=len(
            images)*frame_time, momentum=momentum)

    def tick(self):
        """Animate the effect"""
        super().tick()

        alpha = self.game.alpha - self.start_time

        i = int(alpha // self.frame_time)
        if i < len(self.images):
            self.set_image(self.images[i])
        else:
            self.destroy()


class EffectPlayer:
    """An object which concerns itself with managing the effects"""

    def __init__(self, game: Game) -> None:
        """Initialise the

        :param game: The game which this belongs to
        :type game: Game
        """
        self.sprites = []
        self.game = game
        self.explosion_frames = []
        self.star_image: PhotoImage

    def load_textures(self):
        """Load effect textures"""

        self.explosion_frames = [
            self.game.texture_factory.get_image(f"explosion{i+1}")
            for i in range(3)
        ]
        self.star_image = self.game.texture_factory.get_image("star")

    def tick(self):
        """Update all effects"""
        for sprite in self.sprites:
            sprite.tick()

        self.sprites = Sprite.remove_destroyed(self.sprites)

    def create_stars(self):
        """Initialise the stars in the background"""
        for _ in range(100):
            self.create_star(True)

    def create_star(self, new=False):
        """Add a star to the background

        :param new: Whether this star should be added at
                    the top of the screen or anywhere
        """
        x = randint(0, self.game.w)
        if new:
            y = randint(0, self.game.h)
        else:
            y = -1

        speed = randint(1, 4) * 0.1
        duration = 2*self.game.h / speed

        star = GameEffect(
            self.game,
            self.star_image,
            duration=int(duration),
            momentum=(0, speed)
        )
        star.set_pos((x, y))

        star.send_to_back()
        star.show()

        self.sprites.append(star)

    def create_explosion(self, position=(0, 0)):
        """Create an explosion effect

        :param position: location of the explosion
        """
        for _ in range(randint(1, 3)):
            m = ((random()*2)-1, (random()*2)-1)
            explosion_sprite = AnimatedEffect(
                self.game, self.explosion_frames, frame_time=5, momentum=m)
            explosion_sprite.set_pos(position)
            explosion_sprite.show()

            self.sprites.append(explosion_sprite)

    def splash_text(self, text, duration=50):
        """splash_text.

        :param text:
        :param duration:
        """
        text_img = Font.load_text(self.game.texture_factory, text)
        position = (
            (self.game.w-Font.FONT_WIDTH*len(text)) // 2,
            (self.game.h-Font.FONT_SIZE) // 3
        )

        text_sprite = GameEffect(
            self.game, text_img, duration=duration)
        text_sprite.set_pos(position)
        text_sprite.show()
        self.sprites.append(text_sprite)


class DamageableSprite(GameSprite):
    """Sprite with health points """

    def __init__(self, game: Game, image_name: str, hp=3):
        """Initialise the sprite

        :param game: The game which this belongs to
        :type game: Game
        :param image_name: The name of the image to use for this sprite
        :type image_name: str
        :param hp: The number of hit points this sprite spawns with
        """
        self.image = game.texture_factory.get_image(image_name)
        self.white_image = game.texture_factory.get_image(
            f"{image_name}:white")

        self.hp = hp
        self.animation_frame = 0

        super().__init__(game, self.image)

    def damage(self, amount=1):
        """Decrease number of hit points by an amount

        :param amount:
        """
        if not self.destroyed:
            self.hp -= amount
            self.animation_frame = 5
            if self.hp <= 0:
                self.hp = 0
                self.destroy()
                self.game.effect_player.create_explosion(self.get_pos())

    def tick(self):
        """Update the sprite"""
        super().tick()
        if self.animation_frame > 0:
            self.animation_frame -= 1
            if self.animation_frame % 2 == 0:
                self.set_image(self.image)
            else:
                self.set_image(self.white_image)


if __name__ == "__main__":
    print("!!!")
    print("This is not the main file!")
    print("Pleae run\n\tpython main.py\ninstead!")
    print("!!!")