Python Forum
[PyGame] 2D BoardGame Pygame
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] 2D BoardGame Pygame
#1
Hi all,

I'm trying to develop a 2D board game using PyGame. I'm new to using pygame though and there doesn't seem to be any easy tools for working with grids/2D boards.

Here is a VERY ROUGH prototype of what I want the board to look like.

[Image: xAKwPMq]

As you can see there are 4 main parts to the board. The main grid (black grid), the boxes on the bottom (green boxes) the special button (red box), and the status/score bar in the top left (blue boxes).

The green boxes are going to be the players hand where I will upload my own PGN's in the boxes. There will be 7 different images i.e. one for each cell. Would a spritesheet be best for this?
The main black grid I want to be infinitely big i.e. the player can drag it around. This means that the rest of the elements would have to stay in place i.e. be on a separate layer ontop of the grid.
Also unlike in the prototype all the cells will be the same size (so I guess having a global at the top would make sense). I am not sure what size I should make the window or the cells...but after reading this https://stackoverflow.com/questions/2095...resolution I think just keeping the window resolution at 800x600 and the cell size at 40x40 is a good starting point

The main game mechanic is a drag and drop function where the player drags the images from his hand onto the grid.

The first problem I'm having is how to generate the grid so it's a reactive surface i.e. not just drawn on lines but cells which can be dragged and dropped into.

To generate the grid I'm guessing I would have to use a 2D array but would each cell have to be its own surface? (this seems obviously wrong to me but I really don't know what else to do!) Is there a way to have it as one surface but split it up so I can reference the coordinates of each cell?

For the dragging and dropping I guess there would have to be a function which detects if the image surface coordinates are close to a particular cell and then snap it into place if it is.

Then there is the problem of how to create the grid as a background layer. I've read here https://stackoverflow.com/questions/3363...een-layers that one way to do this is to have different surfaces i.e. one surface for the background layer and one for the rest of the elements.

Then there is the problem of how to make the grid 'infinitely' big or at least appear infinitely big. I'm guessing this is some magic to do with blit and update but again some guidance with this would be helpful.

This is the very basic code I've copied from a tutorial which literally just creates a window and blits an image onto the centre of the screen. Unfortunately most tutorials don't cover the problems I'm having and I can only gather so much from scavenging on StackOverflow!

import pygame as pg
  
pg.init()
  
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('number1.jpg').convert() #create player.image attribute
        self.image.set_colorkey((255,0,255))                  
        self.rect = self.image.get_rect(center=screen_rect.center) #create player.rect attribute from the image and position it to screen center
          
    def draw(self, surf):
        surf.blit(self.image, self.rect)
  
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect) #create player object, run __init__ (dunder init method)
done = False
while not done:
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.draw(screen)
    pg.display.update()
Any help will be much appreciated Big Grin
Reply
#2
(Aug-13-2019, 11:59 AM)Josh_Python890 Wrote: The main game mechanic is a drag and drop function where the player drags the images from his hand onto the grid.
Pygame does not come with any UI features. So anything like this would have to be made from scratch. Which is a lot of work too by the way.

I would first start out by learning and playing around with drag and drop features. This is a text surface but can easily be modified to a square for your needs. Also it would have to be modified to "snap" into the grid instead of just being dropped whenever you release the mouse.

The snap feature could be implemented by defining what grid section the square volume is mostly in, and snap to that grid.

The grid instead of being a grid could be a sequence of squares in which are objects of a class that is a field for things to snap to.
(last code snippet here). Just remove the buffers between the sqaures if you dont want space between them.


At least that is the way i would first tackle this problem.

You have to tackle one thing at a time. Do each one of your issues separately and work on it. A fluent pygamer can write this up in probably a few hours. But a new pygamer could take weeks.
Recommended Tutorials:
Reply
#3
When you want to build something. Just break it down into parts.
Like for grids. There a couple of ways to handle it. One you can do it by math. Two you can handle it by a list of list of rects.

Example
import pygame
import os

from pygame.sprite import Sprite, Group

class State:
    def on_draw(self, surface): pass
    def on_event(self, event): pass
    def on_update(self, delta): pass

class Engine:
    @classmethod
    def setup(cls, caption, width, height, center=False):
        pygame.display.set_caption(caption)
        cls.surface = pygame.display.set_mode((width, height))
        cls.rect = cls.surface.get_rect()
        cls.clock = pygame.time.Clock()
        cls.running = False
        cls.delta = 0
        cls.fps = 30

        if center:
            os.environ['SDL_VIDEO_CENTERED'] = '1'

        cls.state = State()

    @classmethod
    def mainloop(cls):
        cls.running = True
        while cls.running:
            for event in pygame.event.get():
                cls.state.on_event(event)

            cls.state.on_update(cls.delta)
            cls.state.on_draw(cls.surface)
            pygame.display.flip()
            cls.delta = cls.clock.tick(cls.fps)

class SimpleSprite(Sprite):
    @classmethod
    def load_image(cls):
        cls.image = pygame.Surface((38, 38))
        cls.image.fill(pygame.Color('dodgerblue'))

    def __init__(self, position, anchor="topleft"):
        Sprite.__init__(self)
        self.image = SimpleSprite.image
        self.rect = self.image.get_rect()
        setattr(self.rect, anchor, position)

class MouseGrab:
    def __init__(self):
        self.selected = None
        self.home_rect = None
        self.grab_position = None

    def grab(self, pos, sprites):
        for sprite in sprites:
            if sprite.rect.collidepoint(pos):
                self.grab_position = (pos[0] - sprite.rect.x,
                                      pos[1] - sprite.rect.y)
                self.home_rect = sprite.rect.copy()
                self.selected = sprite
                return

    def drop(self, pos, grid):
        if self.selected:
            for col in grid:
                for rect in col:
                    if rect.collidepoint(pos):
                        self.selected.rect.center = rect.center
                        self.selected = None
                        return

    def move(self, pos):
        if self.selected:
            x = pos[0] - self.grab_position[0]
            y = pos[1] - self.grab_position[1]
            self.selected.rect.topleft = x, y


class Example(State):
    def __init__(self):
        self.grid = []
        self.sprites = Group()
        self.mouse = MouseGrab()

        grid_size = 40, 40
        for x in range(0, Engine.rect.width, grid_size[0]):
            col = []
            for y in range(0, Engine.rect.height, grid_size[1]):
                col.append(pygame.Rect(x, y, grid_size[0], grid_size[1]))

            self.grid.append(col)

        sprite = SimpleSprite((60, 60), 'center')
        sprite.add(self.sprites)
        sprite = SimpleSprite((420, 220), 'center')
        sprite.add(self.sprites)

    def on_draw(self, surface):
        surface.fill(pygame.Color('black'))
        self.sprites.draw(surface)

    def on_event(self, event):
        if event.type == pygame.QUIT:
            Engine.running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                self.mouse.grab(event.pos, self.sprites)
        elif event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                self.mouse.drop(event.pos, self.grid)
        elif event.type == pygame.MOUSEMOTION:
            self.mouse.move(event.pos)

def main():
    pygame.init()
    Engine.setup("Example", 800, 600, True)
    SimpleSprite.load_image()
    Engine.state = Example()
    Engine.mainloop()

if __name__ == "__main__":
    main()
99 percent of computer problems exists between chair and keyboard.
Reply
#4
Example. Grid using math.
import pygame
from types import SimpleNamespace

class Grid:
    @staticmethod
    def point(item):
        return SimpleNamespace(x=item[0], y=item[1])

    def __init__(self, rect, size, gap=(0, 0)):
        self.rect = rect
        self.size = Grid.point(size)
        self.gap = Grid.point(gap)
        self.cut = Grid.point((self.size.x + self.gap.x, self.size.y + self.gap.y))

    # Returns None for invalid position
    def get_position(self, x, y):
        if (self.rect.left < x < self.rect.right and
            self.rect.top < y < self.rect.bottom):

            return ((x - self.rect.x) // self.cut.x,
                    (y - self.rect.y) // self.cut.y)

    # Returns None for invalid position
    def get_rect(self, x, y):
        if (self.rect.left < x < self.rect.right and
            self.rect.top < y < self.rect.bottom):

            return pygame.Rect(
                (x - self.rect.x) // self.cut.x * self.cut.x + self.rect.x,
                (y - self.rect.y) // self.cut.y * self.cut.y + self.rect.y,
                self.size.x, self.size.y)

def main():
    rect = pygame.Rect(0, 0, 800, 600)
    grid = Grid(rect, (40, 40))
    print(grid.get_position(12, 22))
    print(grid.get_rect(12, 22))
    print(grid.get_position(83, 58))
    print(grid.get_rect(83, 58))
    print(grid.get_position(100, 700))
    print(grid.get_rect(100, 700))

main()
99 percent of computer problems exists between chair and keyboard.
Reply
#5
(Aug-13-2019, 11:59 AM)Josh_Python890 Wrote: The main black grid I want to be infinitely big i.e. the player can drag it around.
This wouldn't be too hard. You can make a class of grids like Windspar has shown above. When the player moves over to the right to see more cells, just move all the cells to the left. You can have a list of all the cells and go through and draw all the cells from the list. Now the problem with this is that you'll eventually have too many cells to draw and the lag will be crazy. So we only want to draw a cell if it is visible. Before drawing a cell the program can check if the cell is visible, if not there is no point in drawing the cell. You'd also have to apply this same concept to the cards. Even doing this, there could be lag if they build up like 100 cells on the list. To counter this, you can put cells in a different list when they are not visible. When the player explores to a new area, check if any of the cells in that list are in the new area, if so then put them back in the list, otherwise of none of the cells in that list are in the new area, juts create a new cell. I hope this helps with your problem of making an infinite board!
Reply
#6
Here another example. Just couldn't stop myself.
import pygame
import os

from pygame.sprite import Sprite, Group

class State:
    def on_draw(self, surface): pass
    def on_event(self, event): pass
    def on_update(self, delta): pass

class Engine:
    @classmethod
    def setup(cls, caption, width, height, center=False):
        if center:
            os.environ['SDL_VIDEO_CENTERED'] = '1'

        pygame.display.set_caption(caption)
        cls.surface = pygame.display.set_mode((width, height))
        cls.rect = cls.surface.get_rect()
        cls.clock = pygame.time.Clock()
        cls.running = False
        cls.delta = 0
        cls.fps = 30

        cls.state = State()

    @classmethod
    def mainloop(cls):
        cls.running = True
        while cls.running:
            for event in pygame.event.get():
                cls.state.on_event(event)

            cls.state.on_update(cls.delta)
            cls.state.on_draw(cls.surface)
            pygame.display.flip()
            cls.delta = cls.clock.tick(cls.fps)

class Point:
    def __init__(self, *args):
        length = len(args)
        if length == 1:
            if isinstance(args[0], Point):
                self.x = args[0].x
                self.y = args[0].y
            else:
                self.x, self.y = args[0]
        else:
            self.x = args[0]
            self.y = args[1]

    def __add__(self, point):
        return Point(self.x + point.x, self.y + point.y)

    def __sub__(self, point):
        return Point(self.x - point.x, self.y - point.y)

    def __hash__(self):
        return self.x + self.y

    def __iter__(self):
        yield self.x
        yield self.y

    def __repr__(self):
        return 'Point' + str(vars(self))

class ColoredSprite(Sprite):
    images = {}

    @classmethod
    def create_image(cls, color, colorname):
        image = pygame.Surface((38, 38))
        image.fill(color)
        cls.images[colorname] = image
        return image

    @classmethod
    def get_image(cls, colorname):
        if colorname not in pygame.color.THECOLORS.keys():
            colorname = 'dodgerblue'

        if colorname not in cls.images.keys():
            color = pygame.Color(colorname)
            return cls.create_image(color, colorname)
        else:
            return cls.images[colorname]

    def __init__(self, imagename, position, anchor="topleft"):
        Sprite.__init__(self)
        self.image = ColoredSprite.get_image(imagename)
        self.rect = self.image.get_rect()
        setattr(self.rect, anchor, position)

class Grid:
    @classmethod
    def from_slots(cls, position, slots, size, gap=(0, 0)):
        slots = Point(slots)
        size = Point(size)
        gap = Point(gap)
        sz = size + gap
        rect = pygame.Rect(*position, sz.x * slots.x, sz.y * slots.y)
        return cls(rect, size, gap)

    def __init__(self, rect, size, gap=(0, 0), fit=False):
        self.rect = rect
        self.size = Point(size)
        self.gap = Point(gap)
        self.cut = Point(self.size + self.gap)
        self.slots = Point((self.rect.width // self.cut.x,
                                self.rect.height // self.cut.y))
        if fit:
            self.rect.width = self.slots.x * self.cut.x
            self.rect.height = self.slots.y * self.cut.y

        self.show_lines = False
        self.show_boxes = False
        self.build()

    def build(self):
        # Grid Points
        self.points = []
        for i in range(self.rect.x, self.rect.right + 1, self.cut.x):
            self.points.append(((i, self.rect.top), (i, self.rect.bottom)))

        for i in range(self.rect.y, self.rect.bottom + 1, self.cut.y):
            self.points.append(((self.rect.left, i), (self.rect.right, i)))

        # Rects
        self.rects = []
        for x in range(self.rect.left, self.rect.right, self.cut.x):
            for y in range(self.rect.top, self.rect.bottom, self.cut.y):
                self.rects.append(pygame.Rect(x, y, self.size.x, self.size.y))

    # Returns None for invalid position
    def get_position(self, x, y):
        if (self.rect.left < x < self.rect.right and
            self.rect.top < y < self.rect.bottom):

            pos = ((x - self.rect.x) // self.cut.x,
                   (y - self.rect.y) // self.cut.y)

            if self.get_rect(*pos).collidepoint(x, y):
                return pos

    # Returns None for invalid position
    def get_rect(self, x, y, from_screen=False):
        if from_screen:
            if (self.rect.left < x < self.rect.right and
                self.rect.top < y < self.rect.bottom):

                rect = pygame.Rect(
                    (x - self.rect.x) // self.cut.x * self.cut.x + self.rect.x,
                    (y - self.rect.y) // self.cut.y * self.cut.y + self.rect.y,
                    self.size.x, self.size.y)

                if rect.collidepoint(x, y):
                    return rect
        else:
            if 0 <= x < self.slots.x and 0 <= y < self.slots.y:
                return pygame.Rect(x * self.cut.x + self.rect.x,
                                   y * self.cut.y + self.rect.y,
                                   self.size.x, self.size.y)

    def draw(self, surface, color):
        if self.show_lines:
            self.draw_lines(surface, color)
        elif self.show_boxes:
            self.draw_boxes(surface, color)

    def draw_lines(self, surface, color):
        for start, end in self.points:
            pygame.draw.line(surface, color, start, end)

    def draw_boxes(self, surface, color):
        for rect in self.rects:
            pygame.draw.rect(surface, color, rect, 1)

class DragDrop:
    def __init__(self, grid, slots):
        self.grid = grid
        self.slots = slots
        self.selected = None
        self.home_slot = None
        self.home_rect = None
        self.grab_position = None
        self.sprite_groups = None

    def draw(self, surface):
        if self.selected:
            surface.blit(self.selected.image, self.selected.rect)

    def drop(self, pos):
        if self.selected:
            slot = self.grid.get_position(*pos)
            rect = self.grid.get_rect(*pos, True)
            self.selected.add(self.sprite_groups)
            pygame.mouse.set_visible(True)

            if rect and slot != self.home_slot:
                self.slots.swap(slot, self.home_slot)
                s = self.slots.get(self.home_slot)
                if s:
                    x, y = self.home_slot
                    s.rect.center = self.grid.get_rect(x, y).center

                self.selected.rect.center = rect.center
                self.selected = None
            else:
                self.selected.rect = self.home_rect
                self.selected = None

    def grab(self, pos, sprites):
        for sprite in sprites:
            if sprite.rect.collidepoint(pos):
                pygame.mouse.set_visible(False)
                self.grab_position = (pos[0] - sprite.rect.x,
                                      pos[1] - sprite.rect.y)
                self.home_slot = self.grid.get_position(*pos)
                self.home_rect = sprite.rect.copy()
                self.selected = sprite
                self.sprite_groups = sprite.groups()
                sprite.kill()
                return

    def move(self, pos):
        if self.selected:
            x = pos[0] - self.grab_position[0]
            y = pos[1] - self.grab_position[1]
            self.selected.rect.topleft = x, y

class Slots:
    def __init__(self, slots, default_value=None):
        slots = Point(slots)
        self.slots = []
        for x in range(slots.x):
            self.slots.append([default_value] * slots.y)

    def get(self, key):
        key = Point(key)
        return self.slots[key.x][key.y]

    def set(self, key, value):
        key = Point(key)
        self.slots[key.x][key.y] = value

    def swap(self, a, b):
        a = Point(a)
        b = Point(b)
        s = self.slots
        s[a.x][a.y], s[b.x][b.y] = s[b.x][b.y], s[a.x][a.y]

    def __getitem__(self, key):
        return self.slots[key]

    def __setitem__(self, key, value):
        self.slots[key] = value

    def __repr__(self):
        return ('{}\n' * self.size.x).format(*self.slots)

class Example(State):
    def __init__(self):
        self.grid = []
        self.sprites = Group()

        self.grid = Grid(Engine.rect.inflate(-100, -100), (40, 40), (2, 2), True)
        #self.grid = Grid.from_slots((50, 50), (9, 9), (40, 40), (2, 2))
        self.slots = Slots(self.grid.slots)

        self.mouse = DragDrop(self.grid, self.slots)

        self.add_sprite((2, 3), 'firebrick')
        self.add_sprite((8, 6), 'dodgerblue')
        self.add_sprite((1, 7), 'lawngreen')

    def add_sprite(self, pos, colorname):
        pos = Point(pos)
        rect = self.grid.get_rect(pos.x, pos.y)
        sprite = ColoredSprite(colorname, rect.center, 'center')
        sprite.add(self.sprites)
        self.slots.set(pos, sprite)

    def on_draw(self, surface):
        surface.fill(pygame.Color('black'))
        self.grid.draw(surface, pygame.Color('white'))
        self.sprites.draw(surface)
        self.mouse.draw(surface)

    def on_event(self, event):
        if event.type == pygame.QUIT:
            Engine.running = False
        elif event.type == pygame.MOUSEBUTTONDOWN:
            if event.button == 1:
                self.mouse.grab(event.pos, self.sprites)
        elif event.type == pygame.MOUSEBUTTONUP:
            if event.button == 1:
                self.mouse.drop(event.pos)
        elif event.type == pygame.MOUSEMOTION:
            self.mouse.move(event.pos)
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE:
                self.grid.show_lines = not self.grid.show_lines
            elif event.key == pygame.K_b:
                self.grid.show_boxes = not self.grid.show_boxes

def main():
    pygame.init()
    Engine.setup("Example", 800, 600, True)
    Engine.state = Example()
    Engine.mainloop()

if __name__ == "__main__":
    main()
99 percent of computer problems exists between chair and keyboard.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020