You are not logged in.

#1 2010-03-11 16:27:02

Mega-G33k
Member
From: Indiana, USA
Registered: 2009-09-14
Posts: 42

[Python/PyGame] - Need help adding collision detection to snake game

I've been following the tutorials at this website http://en.wordpress.com/tag/pygame-tutorial/ and I finally made the snake game, but I decided for learning purposes to add various features to the game.  Well, I managed to increase the size of the snake to three pixels wide, but now the collision detection is really off.  I've been searching the documentation, my brain, and google to figure this out, and I managed to get close a few times, but it didn't work quite right.  So, if what I said made any sense(which it probably didn't) feel free to give pointers about things in my code besides just the collision detection: style, places where things could be done better, etc. etc.

Thanks in advance!

#! /usr/bin/env python

# Move a worm around the screen. Beware of borders and self!

import pygame
import random

class Worm:
    """ A worm. """

    def __init__(self, surface):
        self.surface = surface
        self.x = surface.get_width() / 2
        self.y = surface.get_width() / 2
        self.length = 1
        self.grow_to = 50
        self.vx = 0
        self.vy = -1
        self.body = []
        self.crashed = False
        self.color = 111, 53, 26

    def event(self, event):
        """ Handle keyboard events that affect the worm. """
        if event.key == pygame.K_UP:
            if self.vy == 1: return
            self.vx = 0
            self.vy = -1
        elif event.key == pygame.K_DOWN:
            if self.vy == -1: return
            self.vx = 0
            self.vy = 1
        elif event.key == pygame.K_LEFT:
            if self.vx == 1: return
            self.vx = -1
            self.vy = 0
        elif event.key == pygame.K_RIGHT:
            if self.vx == -1: return
            self.vx = 1
            self.vy = 0

    def move(self):
        """ Move the worm. """
        self.x += self.vx
        self.y += self.vy

        if (self.x, self.y) in self.body:
            self.crashed = True

        self.body.insert(0, (self.x, self.y))

        if (self.grow_to > self.length):
            self.length += 1

        if (len(self.body) > self.grow_to):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], 3, 3), 0)

        if (len(self.body) > self.length):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], 3, 3), 0)

    def draw(self):
        """ Draw the worm """
        x, y = self.body[0]
        pygame.draw.rect(self.surface, self.color, (x, y, 3, 3), 0)
        x, y = self.body[-1]
        pygame.draw.rect(self.surface, (0, 0, 0), (x, y, 3, 3), 0)

    def position(self):
        return self.x, self.y

    def eat(self):
        self.grow_to += 25

    def hurt(self):
#        self.grow_to -= 25
        self.grow_to = self.grow_to - 25
        self.length = self.length - 25

class Food:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 255, 255

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, 3, 3), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + 3:
            return False
        elif y < self.y or y > self.y + 3:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 3, 3), 0)

class Fire:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 0, 0

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, 4, 4), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + 4:
            return False
        elif y < self.y or y > self.y + 4:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, 4, 4), 0)

# Window dimensions
w = 500
h = 500

screen = pygame.display.set_mode((w, h))
clock = pygame.time.Clock()

score = 0
worm = Worm(screen)
food1 = Food(screen)
fire1 = Fire(screen)
running = True

while running:
    worm.move()
    worm.draw()
    food1.draw()
    fire1.draw()

    if worm.crashed:
        running = False
    elif worm.x <= 0 or worm.x >= w - 1:
        running = False
    elif worm.y <= 0 or worm.y >= h - 1:
        running = False
    elif food1.check(worm.x, worm.y):
        score += 1
        worm.eat()
        print "Score: %d" % score
        food1.erase()
        food1 = Food(screen)
    elif fire1.check(worm.x, worm.y):
        score -= 1
        worm.hurt()
        print "Score: %d" % score
        fire1.erase()
        fire1 = Fire(screen)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            worm.event(event)

    pygame.display.flip()
    clock.tick(100)

Offline

#2 2010-03-11 17:27:29

raf_kig
Member
Registered: 2008-11-28
Posts: 143

Re: [Python/PyGame] - Need help adding collision detection to snake game

you are checking whether your current pixel is in a list containing all pixels your snake currently occupies
1) when updating that list (Worm.body) you don't take into account that our snake is now several pixel wide
2) when checking for a crash (if self.x, self.y in self.body) you don't take into account that your snake is several pixel wide


In case your goal was just to draw a bigger snake the collision detection isn't the problem.
Then the solution would be to decouple internal representation from view, so that you have an internal grid representing the game state and a view drawing it - so making things bigger or smaller would be just a matter of changing the view while leaving the game logic as it is.

Offline

#3 2010-03-11 17:46:34

Mega-G33k
Member
From: Indiana, USA
Registered: 2009-09-14
Posts: 42

Re: [Python/PyGame] - Need help adding collision detection to snake game

I not sure that I understand what you mean.  Do you mean something along these lines:

Game state:

0 0 0 0 0
0 0 0 0 0
0 1 0 1 0
0 0 0 0 0
1 0 0 0 0

where 0 represents an blank space and 1 represents food or obstacles or something.
Although if that is what you mean I don't really know how I would implement that.

Last edited by Mega-G33k (2010-03-11 17:46:58)

Offline

#4 2010-03-11 18:09:25

raf_kig
Member
Registered: 2008-11-28
Posts: 143

Re: [Python/PyGame] - Need help adding collision detection to snake game

What i mean is: the way you have implemented it right now (ignoring the width change)
is based on a 1 : 1 correspondence betweeen a screen pixel and an ingame position, speed is
measured in pixels etc.

So just as a suggestion: Decouple speed and (field|snake|obstacle|...) size from the drawing part.
Define a game field to have a certain size, obstacles to have a certain size, just like it is now.

Now think about the drawing part - try to figure out a way to draw the game that will preserve the proportions of those ingame items (ie the width of the field is 50 obstacles) independent of the window size.

Offline

#5 2010-03-11 22:17:03

Mega-G33k
Member
From: Indiana, USA
Registered: 2009-09-14
Posts: 42

Re: [Python/PyGame] - Need help adding collision detection to snake game

I made a few changes to it, I couldn't think of much else to change, and I still haven't quite thought of how to use your suggestion for collision detection:

#! /usr/bin/env python

# Move a worm around the screen. Beware of borders and self!

import pygame
import random

obstacleSize = 5
sizeOfWorm = obstacleSize
widthOfField = 100 * obstacleSize
heightOfField = 100 * obstacleSize

class Worm:
    """ A worm. """

    def __init__(self, surface):
        self.surface = surface
        self.x = surface.get_width() / 2
        self.y = surface.get_width() / 2
        self.length = 1
        self.grow_to = 50
        self.vx = 0
        self.vy = -1
        self.body = []
        self.crashed = False
        self.color = 111, 53, 26

    def event(self, event):
        """ Handle keyboard events that affect the worm. """
        if event.key == pygame.K_UP:
            if self.vy == 1: return
            self.vx = 0
            self.vy = -1
        elif event.key == pygame.K_DOWN:
            if self.vy == -1: return
            self.vx = 0
            self.vy = 1
        elif event.key == pygame.K_LEFT:
            if self.vx == 1: return
            self.vx = -1
            self.vy = 0
        elif event.key == pygame.K_RIGHT:
            if self.vx == -1: return
            self.vx = 1
            self.vy = 0

    def move(self):
        """ Move the worm. """
        self.x += self.vx
        self.y += self.vy

        if (self.x, self.y) in self.body:
            self.crashed = True

        self.body.insert(0, (self.x, self.y))
        
        if (self.grow_to > self.length):
            self.length += 1

        if (len(self.body) > self.grow_to):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

        if (len(self.body) > self.length):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

    def draw(self):
        """ Draw the worm """
        x, y = self.body[0]
        pygame.draw.rect(self.surface, self.color, (x, y, sizeOfWorm, sizeOfWorm), 0)
        x, y = self.body[-1]
        pygame.draw.rect(self.surface, (0, 0, 0), (x, y, sizeOfWorm, sizeOfWorm), 0)

    def position(self):
        return self.x, self.y

    def eat(self):
        self.grow_to += 25

    def hurt(self):
#        self.grow_to -= 25
        self.grow_to = self.grow_to - 25
        self.length = self.length - 25

class Food:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 255, 255

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + obstacleSize:
            return False
        elif y < self.y or y > self.y + obstacleSize:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

class Fire:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 0, 0

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + obstacleSize:
            return False
        elif y < self.y or y > self.y + obstacleSize:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

screen = pygame.display.set_mode((widthOfField, heightOfField))
clock = pygame.time.Clock()

score = 0
worm = Worm(screen)
food1 = Food(screen)
fire1 = Fire(screen)
running = True

while running:
    worm.move()
    worm.draw()
    food1.draw()
    fire1.draw()

    if worm.crashed:
        running = False
    elif worm.x <= 0 or worm.x >= widthOfField - 1:
        running = False
    elif worm.y <= 0 or worm.y >= heightOfField - 1:
        running = False
    elif food1.check(worm.x, worm.y):
        score += 1
        worm.eat()
        print "Score: %d" % score
        food1.erase()
        food1 = Food(screen)
    elif fire1.check(worm.x, worm.y):
        score -= 1
        worm.hurt()
        print "Score: %d" % score
        fire1.erase()
        fire1 = Fire(screen)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            worm.event(event)

    pygame.display.flip()
    clock.tick(100)

Last edited by Mega-G33k (2010-03-11 22:18:15)

Offline

#6 2010-03-11 23:15:10

raf_kig
Member
Registered: 2008-11-28
Posts: 143

Re: [Python/PyGame] - Need help adding collision detection to snake game

I fail at being clear while leaving the problems for you to solve, I think I just confuse you :-)

OK, I'll give it another shot :-)

Now you've defined some ingame constants but still withOfField is the amount of pixel your field will be wide while at the same time being responsible for how many worms will fit in that field.

Lets introduce a 2nd unit: right now we have pixel for screen estate. Let's add an internal unit (u) to specify the dimensions of the game world and its contents.

Lets say the worm has a width of 1u, obstacles have a width of 2u and the world is 100u X 100u
Can you find a way to translate u to pixel, so that I can change the resolution to 800px X 800px and have everything scale to that size? Changing the resolution shouldn't influence the time it takes the worm to go over the whole field. It should just display everything bigger.

Last edited by raf_kig (2010-03-11 23:16:13)

Offline

#7 2010-03-12 01:54:14

Mega-G33k
Member
From: Indiana, USA
Registered: 2009-09-14
Posts: 42

Re: [Python/PyGame] - Need help adding collision detection to snake game

Here is the new code with scaling implemented pretty well(I think so anyway).

#! /usr/bin/env python

# Move a worm around the screen. Beware of borders and self!

import pygame
import random

widthOfField = 800
heightOfField = 600
sizeUnit = (widthOfField * heightOfField) / 35000
minSizeUnit = 3
if sizeUnit < 3:
    sizeUnit = minSizeUnit
sizeOfWorm = sizeUnit
obstacleSize = sizeUnit
damage = sizeUnit

class Worm:
    """ A worm. """

    def __init__(self, surface):
        self.surface = surface
        self.x = surface.get_width() / 2
        self.y = surface.get_width() / 2
        self.length = sizeUnit
        self.grow_to = sizeUnit * 2
        self.vx = 0
        self.vy = -sizeUnit
        self.body = []
        self.crashed = False
        self.color = 111, 53, 26

    def event(self, event):
        """ Handle keyboard events that affect the worm. """
        if event.key == pygame.K_UP:
            if self.vy == sizeUnit: return
            self.vx = 0
            self.vy = -sizeUnit
        elif event.key == pygame.K_DOWN:
            if self.vy == -sizeUnit: return
            self.vx = 0
            self.vy = sizeUnit
        elif event.key == pygame.K_LEFT:
            if self.vx == sizeUnit: return
            self.vx = -sizeUnit
            self.vy = 0
        elif event.key == pygame.K_RIGHT:
            if self.vx == -sizeUnit: return
            self.vx = sizeUnit
            self.vy = 0

    def move(self):
        """ Move the worm. """
        self.x += self.vx
        self.y += self.vy

        if (self.x, self.y) in self.body:
            self.crashed = True

        self.body.insert(0, (self.x, self.y))

        if (self.grow_to > self.length):
            self.length += sizeUnit

        if (len(self.body) > self.grow_to):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

        if (len(self.body) > self.length):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

    def draw(self):
        """ Draw the worm """
        x, y = self.body[0]
        pygame.draw.rect(self.surface, self.color, (x, y, sizeOfWorm, sizeOfWorm), 0)
        x, y = self.body[-1]
        pygame.draw.rect(self.surface, (0, 0, 0), (x, y, sizeOfWorm, sizeOfWorm), 0)

    def position(self):
        return self.x, self.y

    def eat(self):
        self.grow_to += sizeUnit

    def hurt(self):
#        self.grow_to -= 25
        self.grow_to = self.grow_to - damage
        self.length = self.length - damage

class Food:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 255, 255

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + obstacleSize:
            return False
        elif y < self.y or y > self.y + obstacleSize:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

class Fire:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(5, surface.get_width()-5)
        self.y = random.randint(5, surface.get_height()-5)
        self.color = 255, 0, 0

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, x, y):
        if x < self.x or x > self.x + obstacleSize:
            return False
        elif y < self.y or y > self.y + obstacleSize:
            return False
        else:
            return True

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

screen = pygame.display.set_mode((widthOfField, heightOfField))
clock = pygame.time.Clock()

score = 0
worm = Worm(screen)
food1 = Food(screen)
fire1 = Fire(screen)
running = True

while running:
    worm.move()
    worm.draw()
    food1.draw()
    fire1.draw()

    if worm.crashed:
        running = False
    elif worm.x <= 0 or worm.x >= widthOfField - 1:
        running = False
    elif worm.y <= 0 or worm.y >= heightOfField - 1:
        running = False
    elif food1.check(worm.x, worm.y):
        score += 1
        worm.eat()
        print "Score: %d" % score
        food1.erase()
        food1 = Food(screen)
    elif fire1.check(worm.x, worm.y):
        score -= 1
        worm.hurt()
        print "Score: %d" % score
        fire1.erase()
        fire1 = Fire(screen)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            worm.event(event)

    pygame.display.flip()
    clock.tick((heightOfField/2)/sizeUnit)

So, how can I use what I have so far to implement collision detection.  If you play the game for a while you'll notice that sometimes you'll pass through one half of the food and not collect it.  But I'm not sure how to change it since I'm now using rectangles.  Do you have any pointers for doing that? Thanks a lot for your help so far.

Offline

#8 2010-03-12 02:44:00

Mega-G33k
Member
From: Indiana, USA
Registered: 2009-09-14
Posts: 42

Re: [Python/PyGame] - Need help adding collision detection to snake game

I figured out how to do collision detection.  I draw the worms head, the food, and the obstacles as rect objects, then use the colliderect function(I think it's a function) to determine if they collided. Here's the updated code.

#! /usr/bin/env python

# Move a worm around the screen. Beware of borders and self!

import pygame
import random

widthOfField = 800
heightOfField = 600
sizeUnit = (widthOfField * heightOfField) / 35000
minSizeUnit = 3
if sizeUnit < 3:
    sizeUnit = minSizeUnit
sizeOfWorm = sizeUnit
obstacleSize = sizeUnit
damage = sizeUnit

class Worm:
    """ A worm. """

    def __init__(self, surface):
        self.surface = surface
        self.x = surface.get_width() / 2
        self.y = surface.get_width() / 2
        self.length = sizeUnit
        self.grow_to = sizeUnit * 2
        self.vx = 0
        self.vy = -sizeUnit
        self.body = []
        self.head = None
        self.crashed = False
        self.color = 111, 53, 26

    def event(self, event):
        """ Handle keyboard events that affect the worm. """
        if event.key == pygame.K_UP:
            if self.vy == sizeUnit: return
            self.vx = 0
            self.vy = -sizeUnit
        elif event.key == pygame.K_DOWN:
            if self.vy == -sizeUnit: return
            self.vx = 0
            self.vy = sizeUnit
        elif event.key == pygame.K_LEFT:
            if self.vx == sizeUnit: return
            self.vx = -sizeUnit
            self.vy = 0
        elif event.key == pygame.K_RIGHT:
            if self.vx == -sizeUnit: return
            self.vx = sizeUnit
            self.vy = 0

    def move(self):
        """ Move the worm. """
        self.x += self.vx
        self.y += self.vy

        if (self.x, self.y) in self.body:
            self.crashed = True

        self.body.insert(0, (self.x, self.y))
        self.head = pygame.Rect(self.x, self.y, sizeOfWorm, sizeOfWorm)

        if (self.grow_to > self.length):
            self.length += sizeUnit

        if (len(self.body) > self.grow_to):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

        if (len(self.body) > self.length):
            pop = self.body.pop(-1)
            pygame.draw.rect(self.surface, (0, 0, 0), (pop[0], pop[1], sizeOfWorm, sizeOfWorm), 0)

    def draw(self):
        """ Draw the worm """
        pygame.draw.rect(self.surface, self.color, (self.head))
#        x, y = self.body[0]
#        pygame.draw.rect(self.surface, self.color, (x, y, sizeOfWorm, sizeOfWorm), 0)
        x, y = self.body[-1]
        pygame.draw.rect(self.surface, (0, 0, 0), (x, y, sizeOfWorm, sizeOfWorm), 0)

    def position(self):
        return self.x, self.y

    def eat(self):
        self.grow_to += sizeUnit

    def hurt(self):
#        self.grow_to -= 25
        self.grow_to = self.grow_to - damage
        self.length = self.length - damage

class Food:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(10, surface.get_width()-10)
        self.y = random.randint(10, surface.get_height()-10)
        self.rect = pygame.Rect(self.x, self.y, obstacleSize, obstacleSize)
        self.color = 255, 255, 255

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.rect))

#        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, wormsHead):
        if wormsHead.colliderect(self):
            return True
        else:
            return False

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

class Fire:
    def __init__(self, surface):
        self.surface = surface
        self.x = random.randint(10, surface.get_width()-10)
        self.y = random.randint(10, surface.get_height()-10)
        self.rect = pygame.Rect(self.x, self.y, obstacleSize, obstacleSize)
        self.color = 255, 0, 0

    def draw(self):
        pygame.draw.rect(self.surface, self.color, (self.rect))

#        pygame.draw.rect(self.surface, self.color, (self.x, self.y, obstacleSize, obstacleSize), 0)

    def position(self):
        return self.x, self.y

    def check(self, wormsHead):
        if wormsHead.colliderect(self):
            return True
        else:
            return False

    def erase(self):
        pygame.draw.rect(self.surface, (0, 0, 0), (self.x, self.y, obstacleSize, obstacleSize), 0)

screen = pygame.display.set_mode((widthOfField, heightOfField))
clock = pygame.time.Clock()

score = 0
worm = Worm(screen)
food1 = Food(screen)
fire1 = Fire(screen)
running = True

while running:
    worm.move()
    worm.draw()
    food1.draw()
    fire1.draw()

    if worm.crashed:
        running = False
    elif worm.x <= 0 or worm.x >= widthOfField - 1:
        running = False
    elif worm.y <= 0 or worm.y >= heightOfField - 1:
        running = False
    elif food1.check(worm.head):
        score += 1
        worm.eat()
        print "Score: %d" % score
        food1.erase()
        food1 = Food(screen)
    elif fire1.check(worm.head):
        score -= 1
        worm.hurt()
        print "Score: %d" % score
        fire1.erase()
        fire1 = Fire(screen)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            worm.event(event)

    pygame.display.flip()
    clock.tick((heightOfField/2)/sizeUnit)

There are a few unused methods in there just ignore those.

Last edited by Mega-G33k (2010-03-12 02:44:44)

Offline

Board footer

Powered by FluxBB