General 2d game design book

  • (3 Pages)
  • +
  • 1
  • 2
  • 3

43 Replies - 5897 Views - Last Post: 28 August 2019 - 07:34 AM Rate Topic: -----

#16 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 13 August 2019 - 09:21 AM

View Postmrdman, on 13 August 2019 - 06:39 AM, said:

Where you say never do Main()... do you mean I should have done Game() instead to initialise another game? Or I should have a reset method and then do game.reset(); game.run() ?

So, here is what you did:
def thing(depth=0):
    print("thing({}) called".format(depth))
    thing(depth + 1)  # this is the same as your main call

thing()



Stepping back, this is how I like to do a game loop:
def main():
    # start the game engine
    pygame.init()
    # init the display
    screen = pygame.display.set_mode(DISPLAY_SIZE)
    # init the game state
    state = GameState()
    clock = pygame.time.Clock()

    # loop until state says we're done
    while not state.done:
        # deal with events, then might change state
        handle_event(state)
        # make sure an event didn't tell us to stop
        if not state.done:
            # update our display given the current state of the game
            display(screen, state)
            pygame.display.flip()
            # process changes in state
            update(state)
            clock.tick(60)
    # game loop done
    pygame.quit()



The above is applicable to any game, really. You spin until the game is officially done. You deal with input and state change. You update display.

I'm actually inspired by a few folks here taking a stab at ancient games. I'm currently working on pygame version on Sabotage for fun. But it may be too complex an example. Hmm... I'll be back with a pong example.
Was This Post Helpful? 0
  • +
  • -

#17 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 13 August 2019 - 10:36 AM

View Postbaavgai, on 13 August 2019 - 09:21 AM, said:

View Postmrdman, on 13 August 2019 - 06:39 AM, said:

Where you say never do Main()... do you mean I should have done Game() instead to initialise another game? Or I should have a reset method and then do game.reset(); game.run() ?

So, here is what you did:
def thing(depth=0):
    print("thing({}) called".format(depth))
    thing(depth + 1)  # this is the same as your main call

thing()



Stepping back, this is how I like to do a game loop:
def main():
    # start the game engine
    pygame.init()
    # init the display
    screen = pygame.display.set_mode(DISPLAY_SIZE)
    # init the game state
    state = GameState()
    clock = pygame.time.Clock()

    # loop until state says we're done
    while not state.done:
        # deal with events, then might change state
        handle_event(state)
        # make sure an event didn't tell us to stop
        if not state.done:
            # update our display given the current state of the game
            display(screen, state)
            pygame.display.flip()
            # process changes in state
            update(state)
            clock.tick(60)
    # game loop done
    pygame.quit()



The above is applicable to any game, really. You spin until the game is officially done. You deal with input and state change. You update display.

I'm actually inspired by a few folks here taking a stab at ancient games. I'm currently working on pygame version on Sabotage for fun. But it may be too complex an example. Hmm... I'll be back with a pong example.


Ah ok, I understand.. So the game loop is everything. Once that stops, you are closing the window basically. Any gameover state will be still inside the game loop.

Awesome that you're inspired! I'm loving the speed of making these kinds of games.. A worldwide popular complete game basically in a weekend!
Was This Post Helpful? 0
  • +
  • -

#18 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 13 August 2019 - 11:29 AM

Right, not sure if this is the best design, but I've killed too much of the day on it...

import pygame
import sys
import random


DISPLAY_SIZE = 640, 480
COLOR_BG = (0, 0, 0)
COLOR_FG = (0, 128, 64)


class GameState(object):
    def __init__(self):
        self.done = False
        self.game_over = False
        self.sprites = sprites = pygame.sprite.Group()
        self.sprites.add(Background(self))
        # self.sprites.add(ScoreBoard(self))
        self.sprites.add(Paddle(self))
        self.sprites.add(Ball(self))
        self.sprites.add(GameOver(self))
        self.paddle_move = None
        self.game_begin()

    def draw(self, screen):
        self.sprites.draw(screen)

    def update(self):
        self.sprites.update()

    def game_begin(self):
        self.game_over = False
        self.paddle_move = None
        for x in self.sprites.sprites():
            x.game_begin()

    def game_end(self):
        self.game_over = True
        self.paddle_move = None
        for x in self.sprites.sprites():
            x.game_end()

    def handle_event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.done = True
                break
            elif self.game_over:
                if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                    self.game_begin()
            else:
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_UP:
                        self.paddle_move = (0, -Paddle.SPEED)
                    elif event.key == pygame.K_DOWN:
                        self.paddle_move = (0, Paddle.SPEED)
                elif event.type == pygame.KEYUP:
                    if event.key == pygame.K_UP or event.key == pygame.K_DOWN:
                        self.paddle_move = None


class GameSprite(pygame.sprite.Sprite):
    BG, SCORE, PADDLE, BALL, GAME_OVER = range(5)

    def __init__(self, state, sprite_type):
        pygame.sprite.Sprite.__init__(self)
        self.state = state
        self.sprite_type = sprite_type

    def game_begin(self):
        pass

    def game_end(self):
        pass


class Background(GameSprite):
    WALL_RECT = pygame.Rect(DISPLAY_SIZE[0] - 40, 10, 20, DISPLAY_SIZE[1] - 20)

    def __init__(self, state):
        GameSprite.__init__(self, state, GameSprite.BG)
        self.image = pygame.Surface(DISPLAY_SIZE)
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_BG)
        pygame.draw.rect(self.image, COLOR_FG, Background.WALL_RECT)


class Paddle(GameSprite):
    SPEED = 5

    def __init__(self, state):
        GameSprite.__init__(self, state, GameSprite.PADDLE)
        self.image = pygame.Surface((20, 100))
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_FG)

    def game_begin(self):
        self.rect.x = 20
        self.rect.y = DISPLAY_SIZE[1] // 2 - self.rect.size[1] // 2

    def update(self):
        if not self.state.game_over and self.state.paddle_move:
            r = self.rect.move(self.state.paddle_move)
            if r.top > 10 and r.bottom < DISPLAY_SIZE[1] - 10:
                self.rect = r


class GameOver(GameSprite):
    def __init__(self, state):
        GameSprite.__init__(self, state, GameSprite.GAME_OVER)
        self.font = pygame.font.Font("freesansbold.ttf", 36)
        self.game_begin()

    def game_begin(self):
        self.image = pygame.Surface((1, 1))
        self.rect = self.image.get_rect()

    def game_end(self):
        self.image = self.font.render("Press SPACE to play again", True, COLOR_FG)
        self.rect = self.image.get_rect()
        self.rect.center = (DISPLAY_SIZE[0] // 2, DISPLAY_SIZE[1] // 2)


class Ball(GameSprite):
    SPEED = 5

    def __init__(self, state):
        GameSprite.__init__(self, state, GameSprite.BALL)
        self.image = pygame.Surface((20, 20))
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_FG)
        self.paddle = [x for x in state.sprites.sprites() if x.sprite_type == GameSprite.PADDLE][0]
        self.delta = (0, 0)

    def game_begin(self):
        self.rect.x = DISPLAY_SIZE[0] // 2
        self.rect.y = DISPLAY_SIZE[1] // 2
        self.delta = (Ball.SPEED, Ball.SPEED)

    def update(self):
        if not self.state.game_over:
            self.rect.move_ip(self.delta)
            dx, dy = self.delta
            # print(self.rect.colliderect(self.paddle.rect))
            if self.rect.top < 10 or self.rect.bottom > DISPLAY_SIZE[1] - 10:
                dy *= -1
            if self.rect.right > Background.WALL_RECT.left:
                dx *= -1
            elif pygame.sprite.collide_rect(self, self.paddle):
                dx *= -1
            elif self.rect.left < 10:
                self.state.game_end()
            self.delta = (dx, dy)


def main():
    pygame.init()
    screen = pygame.display.set_mode(DISPLAY_SIZE)
    state = GameState()
    clock = pygame.time.Clock()

    while not state.done:
        state.handle_event()
        if not state.done:
            state.draw(screen)
            pygame.display.flip()
            state.update()
            clock.tick(60)
    pygame.quit()

if __name__ == "__main__":
    main()



This folds in an idea I've been playing with: that a game can just be all sprites, as long as every sprite knows about state and implements a base class. So, that's some very evil OOP there.
Was This Post Helpful? 1
  • +
  • -

#19 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 13 August 2019 - 12:16 PM

Awesome! A lot to take in so will go through it this evening slowly.. sprites is something I've not had a chance to look into yet so will search for some info about them too this evening..
Was This Post Helpful? 0
  • +
  • -

#20 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 14 August 2019 - 01:35 AM

The sprite, as implemented in pygame, is essentially a convenience class. The sprite itself implements image and rect. These funtamentally the two things you need to place something on the screen. It may also implement an update method, which is what you'd need for anything to, say, move around. This is sort of what you'd end up doing yourself, so there's no reason not to comply with the structure and get the extra tools to use.

I will note that rather than the explicit image, I would prefer a draw method overide. You can see me struggling with that one the GameOver sprite.
Was This Post Helpful? 0
  • +
  • -

#21 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 14 August 2019 - 05:21 AM

After a night's sleep, more caffeine, and a dull drive into work, I've redesigned that last pong.

I didn't like how tightly coupled the whole mess was. I was also trying to make all the sprites fit in one bucket and do their own state checks, which is one way to go...

This is more of an MVC design. The state being the model and dead stupid. The other sprites are also stupid, existing only to render and not responding to an update call. The controller talks to the state, makes changes, and readies to the view. More functions here.

I also did kind of a cute hack with this one, which seems to simplify everything else. The controller returns the current state. This allows it to entirely reinitialize the state. Like, you know, a game restart.

import pygame
import sys
import random


DISPLAY_SIZE = 640, 480
COLOR_BG = (0, 0, 0)
COLOR_FG = (0, 128, 64)


class GameState(object):
    def __init__(self):
        self.done = False
        self.game_over = False
        self.paddle = Paddle()
        self.ball = Ball()
        self.sprites = sprites = pygame.sprite.Group()
        self.sprites.add(Background())
        self.sprites.add(self.paddle)
        self.sprites.add(self.ball)


class Background(pygame.sprite.Sprite):
    WALL_RECT = pygame.Rect(DISPLAY_SIZE[0] - 40, 10, 20, DISPLAY_SIZE[1] - 20)

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface(DISPLAY_SIZE)
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_BG)
        pygame.draw.rect(self.image, COLOR_FG, Background.WALL_RECT)


class Paddle(pygame.sprite.Sprite):
    SPEED = 5

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((20, 100))
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_FG)
        self.bounds = (10, DISPLAY_SIZE[1] - 10)
        self.delta = None
        self.rect.x = 20
        self.rect.y = DISPLAY_SIZE[1] // 2 - self.rect.size[1] // 2


class GameOver(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        font = pygame.font.Font("freesansbold.ttf", 36)
        self.image = font.render("Press SPACE to play again", True, COLOR_FG)
        self.rect = self.image.get_rect()
        self.rect.center = (DISPLAY_SIZE[0] // 2, DISPLAY_SIZE[1] // 2)


class Ball(pygame.sprite.Sprite):
    SPEED = 5

    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((20, 20))
        self.rect = self.image.get_rect()
        self.image.fill(COLOR_FG)
        self.rect.x = DISPLAY_SIZE[0] // 2
        self.rect.y = DISPLAY_SIZE[1] // 2
        self.delta = (Ball.SPEED, Ball.SPEED)


def game_stop(state):
    state.game_over = True
    state.paddle.delta = None
    state.ball.delta = None
    state.sprites.add(GameOver())


def move_paddle(state):
    sprite = state.paddle
    if sprite.delta:
        r = sprite.rect.move(sprite.delta)
        if r.top > sprite.bounds[0] and r.bottom < sprite.bounds[1]:
            sprite.rect = r


def move_ball(state):
    ball = state.ball
    if ball.delta:
        ball.rect.move_ip(ball.delta)
        dx, dy = ball.delta
        # print(self.rect.colliderect(self.paddle.rect))
        if ball.rect.top < 10 or ball.rect.bottom > DISPLAY_SIZE[1] - 10:
            dy *= -1
        if ball.rect.right > Background.WALL_RECT.left:
            dx *= -1
        elif pygame.sprite.collide_rect(ball, state.paddle):
            dx *= -1
        elif ball.rect.left < 10:
            game_stop(state)
            return
        ball.delta = (dx, dy)


def controller(state):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            state.done = True
            return state
        elif state.game_over:
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                return GameState()
        else:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP:
                    state.paddle.delta = (0, -Paddle.SPEED)
                elif event.key == pygame.K_DOWN:
                    state.paddle.delta = (0, Paddle.SPEED)
            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_UP or event.key == pygame.K_DOWN:
                    state.paddle.delta = None
    if not state.game_over:
        move_paddle(state)
        move_ball(state)
    return state


def main():
    pygame.init()
    screen = pygame.display.set_mode(DISPLAY_SIZE)
    state = GameState()
    clock = pygame.time.Clock()

    while not state.done:
        if not state.done:
            state.sprites.draw(screen)
            pygame.display.flip()
            state = controller(state)
            if not state.done:
                clock.tick(60)
    pygame.quit()

if __name__ == "__main__":
    main()


Was This Post Helpful? 1
  • +
  • -

#22 NeoTifa   User is offline

  • NeoTifa Codebreaker, the Scourge of Devtester
  • member icon





Reputation: 4550
  • View blog
  • Posts: 19,190
  • Joined: 24-September 08

Re: General 2d game design book

Posted 14 August 2019 - 07:18 AM

About the classes, I didn't see an explanation for that yet. I honestly don't remember the syntax for what looks like python, so how to I cannot say. I can say the purpose and usefulness of them though. So if you think of things in terms of, well, things, everything is its own "object". A class is a classification of objects. Say you have a cup. It's classified as a cup (or, if you wanna get fancy, a coffee cup, which is inherently a special type of cup, and extends from the classification of cup). There are millions of cups, you know what a cup is and what it can or cannot do. This is a class, the concept of "cup". Your particular cup in your hand is an instance of the cup concept. If you want another, just get another instance. In this case, your game itself might be a class, the ball would be a class, there would be 2 instances of a paddle class maybe (for pong), etc. Hope it helps.
Was This Post Helpful? 1
  • +
  • -

#23 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 20 August 2019 - 08:28 AM

So I was busy this weekend and a good few evenings too and managed to finish a chess game which was my ultimate goal since starting this thread. I've now coded the three games I wanted to..
Snake - https://github.com/mrdavidoneill/snake
Tetris - https://github.com/m...idoneill/tetris
Chess - https://github.com/mrdavidoneill/chess

(or at least working prototypes - all need reworking a lot!)

The good news is that it works and has all the rules (en-passant, castle, 3-move repetition etc..) and I managed to do it without looking up anyone else's implementations which was my ultimate goal. Although I'm super excited to see how others have implemented it to see their better ideas!

The bad news is, I'm not at all happy with my code, however, I plan to really get stuck in to it now to refactor, and rework it until I'm happier.
However, I thought I would put it up here for critique first as it may help to show where my weaknesses are and my thought processes and if there are any tips to help me structure my codes in the future for better first drafts.

I seem to get stuck in a rut of long "if this.. if that.. if this.. if that..." functions, whereas other peoples code seems so short and neat. However, it wasn't immediately clear to me how to make a chess move short when there are so many stipulations. So that will be my main goal now and looking for advice to do this.

For example should a " get_possible_squares" function consist of lots of calls to small functions where I pass around pieces & squares etc. Or is that more unreadable than one long function that I have done?

Also I think maybe I'm accepting too many different inputs to functions. eg. string "E4", array coordinates, Piece object, and then dealing with it at the beginning of the function.. maybe it would be better to not do this and just have everything uniform.. and where there is user input, translate it at that point to my chosen coordinates.

Lastly, at the end, today I tried to structure the folders better, but not sure if I messed up as I was forced to search online for something that would make my modules run from outside of the folder. It's this bit here:
import sys, os
testdir = os.path.dirname(__file__)
print(testdir)
srcdir = '../chess'
print(srcdir)
sys.path.insert(0, os.path.abspath(os.path.join(testdir, srcdir)))
print(os.path.abspath(os.path.join(testdir, srcdir)))

I'm not sure why I needed that and don't like that there is something pasted from the internet in my code! But without it, I couldn't use the run.py from outside the folder to run the other modules.

Sorry for the long message and any help as always greatly appreciated!
Was This Post Helpful? 1
  • +
  • -

#24 NeoTifa   User is offline

  • NeoTifa Codebreaker, the Scourge of Devtester
  • member icon





Reputation: 4550
  • View blog
  • Posts: 19,190
  • Joined: 24-September 08

Re: General 2d game design book

Posted 20 August 2019 - 09:53 AM

I'm glad you're keeping up with your research and practicing hard. Most would have given up by now. Keep us updated!
Was This Post Helpful? 1
  • +
  • -

#25 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 20 August 2019 - 11:33 AM

So, again, you'd do better to forget classes for a bit.

Lookup at the tetris, you did the same damn thing again, with main being pointlessly creating an instance of Game which also wasn't really doing you much good. As a refactoring exercise, I merged game.py and main.py and stripped out the class:
from locals import *
from screen import Screen
from piece import Piece
from landed import Grid
import random

def read_best_score():
    with open("best_score.txt", "r") as file:
        return int(file.read())

def write_best_score(score, last_best_score):
    if score > int(last_best_score):
        with open("best_score.txt", "w") as file:
            file.write(str(score))
        return score
    return last_best_score


def gameover(clock, fps):
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if event.key == K_SPACE:
                    # no, no, fucking, no
                    # Game()                  # Restart game
                    return False

        Screen.display_msg("Press SPACE to play again")       # Displays instructions to restart game
        Screen.update()
        clock.tick(fps)
    return True


def main():
    best_score = read_best_score()
    starting_FPS = 3                  # Starting FPS used for resetting the game
    FPS = starting_FPS                # FPS  - option to speed up game
    fpsClock = pygame.time.Clock()    # Clock object
    POINTS = [0, 40, 100, 300, 1200]  # Points for multiple lines cleared
    score = 0

    Screen.start_screen()         # Start screen
    Grid.start_grid()             # Start grid
    # Music.play_bg_music()

    piece = Piece(random.choice(list(Piece.PIECES.keys())))     # Create piece with random shape
    next_shape = random.choice(list(Piece.PIECES.keys()))       # Get a random shape for next piece
    Screen.display_top_info("Next shape:")                      # Display "next shape:" text
    next_piece = Piece(next_shape, "next")                      # Create a "next" piece
    next_piece.draw_next()                                      # Draw next piece on screen

    ##### Game loop #####
    done = False
    while not done:
        for event in pygame.event.get():
            if event.type == QUIT:
                # no
                # pygame.quit()
                # no no sys.exit()
                done = True
                break

            # Arrow changes direction, piece moves towards direction and resets direction to DOWN
            elif event.type == KEYDOWN:
                if event.key == K_LEFT:
                    piece.set_direction(LEFT)
                    piece.move()
                    piece.set_direction(DOWN)
                    break
                elif event.key == K_RIGHT:
                    piece.set_direction(RIGHT)
                    piece.move()
                    piece.set_direction(DOWN)
                    break
                elif event.key == K_DOWN:
                    piece.move()
                    break
                elif event.key == K_SPACE:
                    piece.rotate()
                    break

        # Check if piece move is possible. If LANDED, check if completed rows, update score and best score
        if piece.move() == LANDED:
            completed_rows = Grid.check_complete()
            if completed_rows:
                for row in completed_rows:
                    Grid.flash_row(row)
                    Screen.update()
                    # Music.play_clear_line()
                    fpsClock.tick(FPS)
                    Grid.remove_row(row)
                score += POINTS[len(completed_rows)]
                if score > int(best_score):
                    best_score = score

            Grid.draw_grid()                                        # Draws grid
            piece = Piece(next_shape)                               # Creates new piece
            next_shape = random.choice(list(Piece.PIECES.keys()))   # Creates new shape
            Screen.draw_top_bar()                                   # Draws top bar
            Screen.display_top_info("Next shape:")                  # Writes "Next shape:"
            next_piece = Piece(next_shape, "next")                  # Creates "next" piece
            next_piece.draw_next()                                  # Draws next piece

            # Check if gameover, if so update the best_score.txt if necessary
            if piece.gameover:
                best_score = write_best_score(score, best_score)
                done = gameover(fpsClock, FPS)

        if not done:
            # Draw bottom bar with updated score
            Screen.draw_btm_bar()
            Screen.display_btm_info(f"Score: {score}  (Best score: {best_score})")  # Display score

            Screen.update()
            fpsClock.tick(FPS)
    pygame.quit()


if __name__ == "__main__":
    main()



As an aside, I also removed that hideous recursive restart. There are still a myriad of issues here, but just wanted to demonstrate how little it takes to excise the unnecessary class.
Was This Post Helpful? 1
  • +
  • -

#26 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 20 August 2019 - 12:22 PM

Yes sorry about! I did the Tetris just before you gave feedback on the snake one.. and didn't get a chance to go back to it yet. Thanks for the example.. that's so much clearer than what I did yes.. I think I'll go back to the beginning and refactor snake first, then Tetris before tackling the more complicated chess one.. maybe that will be better for me to get to grips on the easiest of the three first.
Was This Post Helpful? 0
  • +
  • -

#27 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 21 August 2019 - 02:34 AM

Here's my snake reworked following your suggestions.. is this along the right lines? It certainly feels better but as I have not got much experience, it's sometimes difficult to tell!

import pygame, sys, random                  # sys for closing programm.  random for Apple position
from pygame.locals import *                 # for using pygame keywords like QUIT or KEYDOWN

# =========== User changeable data =========== #
GRID_SIZE = 10                      # pixels
GRID_WIDTH, GRID_HEIGHT = 30, 30    # grid cells
STARTING_FPS = 5                    # Starting speed

# =========== CONSTANTS =========== #

# Score bar height bar in pixels
BOTTOMBAR = 50
# Size of playing area in pixels
SCREEN_WIDTH = GRID_WIDTH * GRID_SIZE
SCREEN_HEIGHT = GRID_HEIGHT * GRID_SIZE
# Size of window in pixels
WINDOW_SIZE   = (SCREEN_WIDTH, SCREEN_HEIGHT + BOTTOMBAR)    # Tuple of window size()
# Midpoint of grid in cells
GRID_MID_Y = SCREEN_HEIGHT / 2 / GRID_SIZE  # Mid point on y axis in Grid coordinates
GRID_MID_X = SCREEN_WIDTH / 2 / GRID_SIZE   # Mid point on x axis in Grid coordinates

# Confirm Screen size is divisible by the grid size
assert(SCREEN_WIDTH % GRID_SIZE == 0)
assert(SCREEN_HEIGHT % GRID_SIZE == 0)

# =========== COLOURS =========== #
BLACK = (  0,  0,  0)
WHITE = (255,255,255)
GREY =  (155,155,155)
RED   = (255,  0,  0)

# =========== KEYWORDS =========== #
x = "x"
y = "y"
UP = "up"
DOWN = "down"
LEFT = "left"
RIGHT = "right"

####################################

class Game:

    score = 0
    snake = None
    apple = None
    fps = STARTING_FPS
    fpsClock = None

    @classmethod
    def run(cls):
        """ Initialise pygame, start Screen and reset Game with new snake and apple """
        pygame.init()
        pygame.display.set_caption("Snake")
        cls.fpsClock = pygame.time.Clock()
        Screen.reset_screen()
        cls.reset()

        ##### Game loop #####
        while True:
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()

                # Arrow controls direction, and blocks going directly to direction it came from
                elif event.type == KEYDOWN:
                    if event.key == K_LEFT and cls.snake.get_direction() != RIGHT:
                        cls.snake.set_direction(LEFT)
                    elif event.key == K_RIGHT and cls.snake.get_direction() != LEFT:
                        cls.snake.set_direction(RIGHT)
                    elif event.key == K_UP and cls.snake.get_direction() != DOWN:
                        cls.snake.set_direction(UP)
                    elif event.key == K_DOWN and cls.snake.get_direction() != UP:
                        cls.snake.set_direction(DOWN)
                    break

            if not cls.snake.move():
                Game.gameover()

            if cls.snake.eating(cls.apple[0], cls.apple[1]):
                cls.score += 1
                cls.set_apple_pos()
                Screen.draw_apple(cls.apple)
                Screen.draw_btm_bar()
                Screen.display_btm_msg(f"Score: {cls.score}")
                cls.snake.grow()
                cls.fps += 1

            pygame.display.update()
            cls.fpsClock.tick(cls.fps)

    @classmethod
    def gameover(cls):
        """ Check for quit, or space bar which resets game.
            Display game-over message over image of last lost position """
        while True:
            for event in pygame.event.get():
                if event.type == QUIT:
                    pygame.quit()
                    sys.exit()
                elif event.type == KEYDOWN:
                    if event.key == K_SPACE:
                        Screen.reset_screen()
                        cls.reset()
                        return

            Screen.display_centre_msg("Press SPACE to play again")
            pygame.display.update()
            cls.fpsClock.tick(cls.fps)

    @classmethod
    def set_apple_pos(cls):
        """ Set class property 'apple' to a random position within the grid """
        cls.apple = (random.randint(0, GRID_WIDTH - 1),
                     random.randint(0, GRID_HEIGHT - 1))

    @classmethod
    def reset(cls):
        """ Reset game state with score of 0, a new snake, and a random apple position,
            then draw apple to screen and display score """
        cls.score = 0
        cls.snake = Snake()
        cls.set_apple_pos()
        Screen.draw_apple(cls.apple)
        Screen.display_btm_msg(f"Score: {cls.score}")


class Snake:
    HEAD = 0
    COLOR = BLACK

    def __init__(self):
        """ Initialise a snake with a 3 block body.
            Direction set to left, """

        self.body = [{x: GRID_MID_X + 0, y: GRID_MID_Y},
                     {x: GRID_MID_X + 1, y: GRID_MID_Y},
                     {x: GRID_MID_X + 2, y: GRID_MID_Y}]
        self.direction_x = -1
        self.direction_y = 0

    def draw(self):
        """ Draw snake on display surface """
        for block in self.body:
            block_position = (block[x] * GRID_SIZE, block[y] * GRID_SIZE, GRID_SIZE, GRID_SIZE)
            pygame.draw.rect(Screen.display, Snake.COLOR, block_position)

    def erase(self):
        """ Erase snake from display surface """
        for block in self.body:
            block_position = (block[x] * GRID_SIZE, block[y] * GRID_SIZE, GRID_SIZE, GRID_SIZE)
            pygame.draw.rect(Screen.display, Screen.game_bg_color, block_position)

    def move(self):
        """ Erase snake from display surface, delete last tail position and add new head position in direction
            heading.  Then draw snake in new position.
            Calculate if it's outside the screen, or hit its own tail and if so: stop snake and return False,
            else return True """
        self.erase()
        self.body.pop()
        new_head = [{x: self.body[Snake.HEAD][x] + self.direction_x, y: self.body[Snake.HEAD][y] + self.direction_y}]
        self.body = new_head + self.body
        self.draw()
        if self.body[Snake.HEAD][x] < 0 or self.body[Snake.HEAD][x] == GRID_WIDTH:
            self.stop()
            return False
        if self.body[Snake.HEAD][y] < 0 or self.body[Snake.HEAD][y] == GRID_HEIGHT:
            self.stop()
            return False
        if self.hit_body():
            return False
        return True

    def stop(self):
        """ Stop snake by setting it's X and Y direction to 0 """
        self.direction_x = 0
        self.direction_y = 0

    def set_direction(self, dir):
        """ Set its direction to given argument of one of:
            UP, DOWN, RIGHT, LEFT """
        if dir == UP:
            self.direction_x = 0
            self.direction_y = -1
        elif dir == DOWN:
            self.direction_x = 0
            self.direction_y = 1
        elif dir == RIGHT:
            self.direction_x = 1
            self.direction_y = 0
        elif dir == LEFT:
            self.direction_x = -1
            self.direction_y = 0

    def get_direction(self):
        """ Return the direction the snake is headed """
        if self.direction_x == 1:
            return RIGHT
        elif self.direction_x == -1:
            return  LEFT
        elif self.direction_y == -1:
            return UP
        elif self.direction_y == 1:
            return DOWN

    def eating(self, apple_x, apple_y):
        """ Return True if snake head has hit the apple """
        if self.body[Snake.HEAD][x] == apple_x and self.body[Snake.HEAD][y] == apple_y:
            return True

    def grow(self):
        """ Increase its body size by one """
        new_head = [{x: self.body[Snake.HEAD][x] + self.direction_x, y: self.body[Snake.HEAD][y] + self.direction_y}]
        self.body = new_head + self.body

    def hit_body(self):
        """ Return True if head hits its own body, else return False """
        for block in self.body[1:]:
            if block[x] == self.body[Snake.HEAD][x] and block[y] == self.body[Snake.HEAD][y]:
                return True
        else:
            return False


class Screen:

    display = pygame.display.set_mode(WINDOW_SIZE)
    btm_bar_color = WHITE
    game_bg_color = GREY
    btm_bar = (0, SCREEN_HEIGHT, SCREEN_WIDTH, BOTTOMBAR)

    @classmethod
    def reset_screen(cls):
        """ Starts the screen with score bar at bottom of window,
            then draws to display """

        cls.fill_background()
        cls.draw_btm_bar()

    @classmethod
    def fill_background(cls):
        """ Fills background with game_bg_color """
        cls.display.fill(cls.game_bg_color)  # Fill window with game background colour


    @classmethod
    def draw_btm_bar(cls):
        """ Draws bottom bar on display surface """
        pygame.draw.rect(cls.display, cls.btm_bar_color, cls.btm_bar)

    @classmethod
    def draw_apple(cls, pos):
        """ Draws apple to the display surface """
        x, y = pos
        block_position = (x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE, GRID_SIZE)
        pygame.draw.rect(cls.display, RED, block_position)

    @classmethod
    def display_btm_msg(cls, msg):
        """ Displays the score onto the bottom of the window where the screen bar is """
        fontObj = pygame.font.Font("freesansbold.ttf", min(int(SCREEN_WIDTH / 10), 32))
        score_surface = fontObj.render(msg, True, BLACK)
        score_rect = score_surface.get_rect()
        score_rect.center = (SCREEN_WIDTH / 2, SCREEN_HEIGHT + (BOTTOMBAR / 2))
        cls.display.blit(score_surface, score_rect)

    @classmethod
    def display_centre_msg(cls, msg):
        """ Displays message showing how to start new game """
        fontObj = pygame.font.Font("freesansbold.ttf", min(int(SCREEN_WIDTH / 14), 25))
        new_game_surface = fontObj.render(msg, True, BLACK)
        new_game_rect = new_game_surface.get_rect()
        new_game_rect.center = (SCREEN_WIDTH / 2,SCREEN_HEIGHT /2)
        cls.display.blit(new_game_surface, new_game_rect)


if __name__ == "__main__":
    Game.run()



Is what I have done with the screen and game classes viable? ie. no instances as there's only ever going to be one screen or one game.. and then using class methods. I guess it's using classes more like a collection of connected code rather than OOP. Do people do this? Or am I committing a faux pas?
Was This Post Helpful? 0
  • +
  • -

#28 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 21 August 2019 - 02:45 AM

... Oh and I've just realised why that recursive restart is bad...! Doing it that way, every time I'm calling main inside main, I'm keeping all the game states on the stack, and then piling another main on top. And again.. and again.. without ever letting go of anything. Doh! What a stupid thing to do and completely unnecessary!
Was This Post Helpful? 0
  • +
  • -

#29 baavgai   User is online

  • Dreaming Coder
  • member icon


Reputation: 7470
  • View blog
  • Posts: 15,492
  • Joined: 16-October 07

Re: General 2d game design book

Posted 21 August 2019 - 05:44 AM

So, again, a class's raison d'etre is to store state. The methods of a class exist because the have a direct relation to that state. If you have a "class method" or a "static method" you are grouping a function with that class that you feel very strongly should be directly associated with that class. Generally, in that respect, you're probably wrong.

If you have a method in your class that doesn't reference self, it simply doesn't need to be there. Conversely, you can write an entire program with zero classes. Considering how far you've fallen to the one side, you might want to play with the other. (Remember, foo.bar() is really just a friendly way of writing: def bar(foo):.) Hmm... you didn't happen to do too much Java programming in the past?
Was This Post Helpful? 0
  • +
  • -

#30 mrdman   User is offline

  • New D.I.C Head

Reputation: 4
  • View blog
  • Posts: 21
  • Joined: 21-July 19

Re: General 2d game design book

Posted 21 August 2019 - 06:20 AM

I've never used Java no.. But I have just come off the back of using a lot of Jack language (from the Nand2Tetris courses) which is similar in that everything has to be inside a class, with main class being run automatically etc.

I think rewriting it without any classes is a great idea, and will do me the world of good, so I will do that next. I've never coded the exact same thing in different ways before but that will be great to really get to grips with structuring things in different ways, without any of the distractions of thinking about the actual game / visuals so to be speak.
Was This Post Helpful? 0
  • +
  • -

  • (3 Pages)
  • +
  • 1
  • 2
  • 3