Posts: 8
Threads: 6
Joined: Sep 2017
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.
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
Posts: 5,151
Threads: 396
Joined: Sep 2016
Aug-13-2019, 12:32 PM
(This post was last modified: Aug-13-2019, 12:33 PM by metulburr.)
(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:
Posts: 544
Threads: 15
Joined: Oct 2016
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.
Posts: 544
Threads: 15
Joined: Oct 2016
Aug-13-2019, 06:25 PM
(This post was last modified: Aug-13-2019, 06:25 PM by Windspar.)
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.
Posts: 479
Threads: 86
Joined: Feb 2018
(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!
Posts: 544
Threads: 15
Joined: Oct 2016
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.
|