Home Backend Development Python Tutorial Building Something That&#s Not a Todo App: An Online Multiplayer Snake Game

Building Something That&#s Not a Todo App: An Online Multiplayer Snake Game

Aug 21, 2024 am 06:19 AM

I recently stumbled upon a quote by tech influencer "the primeagen" I don't remember it exactly but as I remember it goes like this :

"If you're not failing at what you do, then you're not learning."

This got me thinking about my coding journey. I've become quite comfortable with building backends, to the point where even writing import express from 'express'; has become a chore.

Instead of going through the canon event of learning yet another JavaScript framework in order to build my millionth revolutionary todo app (because clearly, the world needs more of those), I decided to do something else. I'd been reading about the WebSocket protocol and found its ability to handle bidirectional asynchronous messages between server and client fascinating. I wanted to build something with it, but I needed a break from JavaScript.

After some consideration, I settled on a simple multiplayer 2D game. It would involve calculations (collision detection), data structures (linked lists, hashmaps), and player synchronization. A snake game seemed perfect, with a few simple rules:

  1. Eating a fruit makes you grow and adds 1 to your score

  2. Crashing into another player's body makes you shrink, resets your position randomly, and zeros your score

  3. Head-to-head collisions cause both players to shrink, reset their positions, and zero their scores

All these calculations happen server-side to prevent players from tampering with the game logic. We'll use Python 3 with Pygame for graphics and the websockets library for handling asynchronous messages through asyncio.

Now, let's dive into the code that you may find it messy because remember the first rule of programming:

"If it works, don't touch it."

You’ve read enough of my yapping, let’s move on to the fun part: Coding. But if you want to skip the chatter and dive straight in, just head over to the GitHub repo.
If you want to contribute , feel free to open an issue or submit a pull request. Any improvements or bug fixes are welcome!

First, we define our data structures:

class Object :
    def __init__(self , x : float , y :float ,  width:int , height :int):

        ####################################
        #  init object's size and postion  #           
        ####################################
        self.x = x
        self.y = y 
        self.height = height
        self.width = width

    def render(self , screen , color) :
        pygame.draw.rect(screen ,color ,pygame.Rect(self.x , self.y , self.width , self.height))

class Player(Object) :

    def __init__(self, x: float, y: float, width: int, height: int):
        super().__init__(x, y, width, height)
        self.next = None
        self.prev = None
        self.tail = self
        self.direction = 'LEFT'
        self.length = 1
        self.color = 'red'

    # move the Snake to a certain direction
    # the "changed" will be a way to tell  either to continue in the same direction 
    # or change the direction of the head to the new direction
    # it is used in the game

    def change_direction(self, keys):
        changed = False
        if self.direction in ['LEFT', 'RIGHT']:
            if keys[pygame.K_w] and self.direction != 'DOWN':
                self.direction = 'UP'
                changed = True
            elif keys[pygame.K_s] and self.direction != 'UP':
                self.direction = 'DOWN'
                changed = True
        elif self.direction in ['UP', 'DOWN']:
            if keys[pygame.K_a] and self.direction != 'RIGHT':
                self.direction = 'LEFT'
                changed = True
            elif keys[pygame.K_d] and self.direction != 'LEFT':
                self.direction = 'RIGHT'
                changed = True
        return changed  

    # move the Snake to a certain direction with a certain speed
    def move(self, screen, dt):
        speed = 150 * dt 
        if self.direction == 'UP':
            self.move_all(screen, 0, -speed)
        elif self.direction == 'DOWN':
            self.move_all(screen, 0, speed)
        elif self.direction == 'LEFT':
            self.move_all(screen, -speed, 0)
        elif self.direction == 'RIGHT':
            self.move_all(screen, speed, 0)
    def bound(self , screen) :
        if self.y < 0  :
            self.y = screen.get_height()
        if self.y > screen.get_height() :
            self.y = 0

        if self.x < 0  :
            self.x = screen.get_width()
        if self.x > screen.get_width() :
            self.x = 0

    def get_pos(self) :
        arr = []
        current = self 
        while current :
            arr.append([current.x , current.y])
            current = current.next
        return arr

    # move the snake and its body to some coordinates 
    def move_all(self, screen, dx, dy):
        # Store old positions
        old_positions = []
        current = self
        while current:
            old_positions.append((current.x, current.y))
            current = current.next

        # Move head
        self.x += dx
        self.y += dy
        self.bound(screen)
        # self.render(screen, self.color)

        # Move body
        current = self.next
        i = 0
        while current:
            current.x, current.y = old_positions[i]
            current.bound(screen)
            # current.render(screen, self.color)
            current = current.next
            i += 1           

    def add(self ):
        new = Player(self.tail.x+self.tail.width+10 ,
                     self.tail.y ,self.tail.width ,                    
                      self.tail.height)
        new.prev = self.tail 
        self.tail.next = new
        self.tail = new
        self.length +=1

    def shrink(self , x , y):
        self.next = None 
        self.tail = self 
        self.length = 1
        self.x = x 
        self.y = y

    # used for the and the opponent player when 
    # receiving its coordinates
    def setall(self , arr) :
        self.x = arr[0][0]
        self.y = arr[0][1]
        self.next = None
        self.tail = self
        current = self 
        for i in range(1 , len(arr)) :
            x = arr[i][0]
            y = arr[i][1]
            new = Player(x ,y ,self.width , self.height)
            current.next = new 
            self.tail = new
            current = current.next 

   # render the whole snake on the screen 
   # used for both the current player and the opponent
    def render_all(self, screen, color):
        current = self        
        if self.next : 
            self.render(screen,'white')
            current = self.next
        while current :
            current.render(screen , color) 
            current = current.next

Copy after login

The Object class is a base class for game objects, while the Player class extends it with snake-specific functionality. The Player class includes methods for changing direction, moving, growing, shrinking, and rendering the snake.

Next, we have the game logic:

import pygame 
from objects import Player
import websockets 
import asyncio
import json

uri = 'ws://localhost:8765'

pygame.font.init() 


# Render the text on a transparent surface
font = pygame.font.Font(None, 36)

# playing the main theme (you should hear it)
def play() :
    pygame.mixer.init()
    pygame.mixer.music.load('unknown.mp3')
    pygame.mixer.music.play()

def stop():
    if pygame.mixer.music.get_busy():  # Check if music is playing
        pygame.mixer.music.stop()

# initialize players and the fruit
def init(obj) :
    print(obj)
    player = Player(*obj['my'][0])
    opp = Player(*obj['opp'][0] )
    food = Food(*obj['food'])
    return (player , opp , food)

async def main():

    async with websockets.connect(uri) as ws:
        choice = int(input('1 to create a room \n2 to join a room\n>>>'))
        room_name = input('enter room name: ')
        await ws.send(json.dumps({
            "choice" : choice ,
            "room" : room_name
        }))

        ## waiting for the other player to connecet
        res = {}
        while True:
            res = await ws.recv()
            try:
                res = json.loads(res)

                break 
            except Exception as e:
                pass

        player, opp, food = init(res) 
        pygame.init()
        screen = pygame.display.set_mode((600, 400))
        clock = pygame.time.Clock()
        running = True
        dt = 0
        play()
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False

            screen.fill("black")
            my_pos = {
                'pos': player.get_pos(), 
                'len': player.length
            }
            await ws.send(json.dumps(my_pos))
            response = await ws.recv()
            response = json.loads(response)

            # Update food position
            pygame.draw.rect(screen ,'green' ,pygame.Rect(response['food'][0] ,response['food'][1] ,20, 20))
            # Handle actions
            if response['act'] == 'grow':
                player.add()
            elif response['act'] == 'shrinkall':
                player.shrink(*response['my'][0][0:2])
                opp.shrink(*response['opp'][0][0:2])
            elif response['act'] == 'shrink':
                player.shrink(*response['my'][0][0:2])
                # restarting the song each time you bump into the other player
                stop()
                play()
            else:
                opp.setall(response['opp'])

            # Render everything once per frame 
            player.render_all(screen, 'red')
            opp.render_all(screen, 'blue')

            ## score
            ## x | y => x you , y opponent
            text = font.render(f'{response["my_score"]} | {response["other_score"]}', True, (255, 255, 255))

            screen.blit(text, (0, 0)) 
            pygame.display.flip()

            keys = pygame.key.get_pressed()
            changed = player.change_direction(keys)
            # keep moving in the same direction if it is not changed
            if not changed:
                player.move(screen, dt)

            dt = clock.tick(60) / 1000

        pygame.quit()

asyncio.run(main())
Copy after login

this will connect you the server and let you create and join a room , it will send an update of your position to the server and get the opponent's position and the fruit position and an act which will be an order to the player to either shrink or grow

Finally, the server code:

import asyncio
import websockets 
import random
import json

def generate_food_position(min_x, max_x, min_y, max_y):
    x = random.randint(min_x, max_x)
    y = random.randint(min_y, max_y)
    return [x, y, 20, 20]

rooms  = {}

def collide(a , b , width):
    return (abs(a[0] - b[0]) < width and
            abs(a[1] - b[1]) < width)

# detecting possible collides : 
def collides(a , b , food ) :
    head_to_head = collide(a[0] , b[0] ,30) ;
    head_to_food = collide(a[0] , food ,25  ) 
    head_to_body = False 
    this_head = a[0]
    for part in b :
        if collide(this_head , part  ,30) :
            head_to_body = True 
            break 

    return (head_to_head , head_to_body , head_to_food)

# return response as (act ,opponents position(s) , my position(s) , the food and the scores)
def formulate_response(id , oid , food , roomName) :
    this= rooms[roomName][id]['pos']
    other= rooms[roomName][oid]['pos']
    hh , hb , hf = collides(this ,other ,food)
    act = 'None'
    if hh :
        act = 'shrink' 
        rooms[roomName][id]['pos'] = initPlayer()
        rooms[roomName][id]['score'] =0 
        # rooms[roomName][oid]['pos'] = initPlayer()
        # rooms[roomName][oid]['respawn'] = True

    elif hb :
        act = 'shrink' 
        rooms[roomName][id]['pos'] = initPlayer()
        rooms[roomName][id]['score'] = 0   
    elif hf :
        act = 'grow'
        rooms[roomName]['food'] =  generate_food_position(20, 580, 20, 380) 
        rooms[roomName][id]['score']+=1
    return  {
        'act' : act , 
        'opp' : rooms[roomName][oid]['pos'] , 
        'my'  : rooms[roomName][id]['pos'] ,
        'food': rooms[roomName]['food'] ,
        'my_score' : rooms[roomName][id]['score'] ,
        'other_score' : rooms[roomName][oid]['score']
    }




def initPlayer():
    return [[random.randint(30 , 600 ) , random.randint(30 , 400 ) , 30 , 30 ]] 


async def handler(websocket) :

    handshake = await websocket.recv()
    handshake = json.loads(handshake)  

    roomName = handshake["room"]
    if handshake['choice'] == 1 :
        rooms[roomName] = {}
        rooms[roomName]['food'] =generate_food_position(30 ,570 ,30 ,370)

    rooms[roomName][websocket.id] = {
            'socket' : websocket , 
            'pos' : initPlayer() , 
            'respawn' : False ,
            'score' :0
    }
    if len(rooms[roomName]) >= 3 :
        await broadcast(rooms[roomName])
    id = websocket.id

    while True :
        room = rooms[roomName]
        this_pos = await websocket.recv()

        ## synchrnisation issue with this 
        ## after couple of times they collide head to head
        ## the cordinates shown on the screen aren't same 
        ## as the server 

        if room[id]['respawn']==True :
            rooms[roomName][id]['respawn'] = False


            # generate response :
            response = {
                'act' :  'shrinkall', 
                'my'  :  room[id]['pos']  , 
                'opp' :  get_other_pos(get_other_id(id ,room),room),
                'food': room['food']
            } 
            await websocket.send(json.dumps(response))

        else :
            # update player position  

            this_pos = json.loads(this_pos)

            rooms[roomName][id]['pos'] = this_pos['pos']
            rooms[roomName][id]['len'] = this_pos['len']

            other_id = get_other_id(id , room)
            food = room['food']
            response = formulate_response(id ,other_id ,food  ,roomName)  


        await websocket.send(json.dumps(response))

def get_other_id(id , room) :
    for thing in room.keys():
        if thing != 'food' and thing != id :
            return thing
def get_other_pos(id , room) : 
    return room[id]['pos']

async def broadcast(room) :
    for thing in room.keys() :
        if thing!= 'food' :
            init = {
                'my' : room[thing]['pos'] , 
                'opp' : room[get_opp(thing, room)]['pos'] ,
                'food': room['food']
            }
            await room[thing]['socket'].send(json.dumps(init))

def get_opp(id  , room) :
     for thing in room.keys() :
        if thing!= 'food' and thing != id:
            return thing


async def main():
    async with websockets.serve(handler , 'localhost' ,8765 ):
        await asyncio.Future()



if __name__ == '__main__' :
    print('listenning  ... ')
    asyncio.run(main())
Copy after login

it handles player movements, collisions, and interactions. It generates food, detects collisions, and synchronizes game state between players. The server also manages player connections and pushing game updates

Building Something That
And there you have it a multiplayer snake game that could be the next big thing in the world of gaming. Who knows, it might even make its way to the next big tech conference!
For now, why not take a spin and see what you can add? Check out the GitHub repo and make your mark on the next big thing in snake games.
Happy coding!

The above is the detailed content of Building Something That&#s Not a Todo App: An Online Multiplayer Snake Game. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Roblox: Bubble Gum Simulator Infinity - How To Get And Use Royal Keys
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
Mandragora: Whispers Of The Witch Tree - How To Unlock The Grappling Hook
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
Nordhold: Fusion System, Explained
3 weeks ago By 尊渡假赌尊渡假赌尊渡假赌

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1667
14
PHP Tutorial
1273
29
C# Tutorial
1255
24
Python: Games, GUIs, and More Python: Games, GUIs, and More Apr 13, 2025 am 12:14 AM

Python excels in gaming and GUI development. 1) Game development uses Pygame, providing drawing, audio and other functions, which are suitable for creating 2D games. 2) GUI development can choose Tkinter or PyQt. Tkinter is simple and easy to use, PyQt has rich functions and is suitable for professional development.

Python vs. C  : Learning Curves and Ease of Use Python vs. C : Learning Curves and Ease of Use Apr 19, 2025 am 12:20 AM

Python is easier to learn and use, while C is more powerful but complex. 1. Python syntax is concise and suitable for beginners. Dynamic typing and automatic memory management make it easy to use, but may cause runtime errors. 2.C provides low-level control and advanced features, suitable for high-performance applications, but has a high learning threshold and requires manual memory and type safety management.

Python and Time: Making the Most of Your Study Time Python and Time: Making the Most of Your Study Time Apr 14, 2025 am 12:02 AM

To maximize the efficiency of learning Python in a limited time, you can use Python's datetime, time, and schedule modules. 1. The datetime module is used to record and plan learning time. 2. The time module helps to set study and rest time. 3. The schedule module automatically arranges weekly learning tasks.

Python vs. C  : Exploring Performance and Efficiency Python vs. C : Exploring Performance and Efficiency Apr 18, 2025 am 12:20 AM

Python is better than C in development efficiency, but C is higher in execution performance. 1. Python's concise syntax and rich libraries improve development efficiency. 2.C's compilation-type characteristics and hardware control improve execution performance. When making a choice, you need to weigh the development speed and execution efficiency based on project needs.

Which is part of the Python standard library: lists or arrays? Which is part of the Python standard library: lists or arrays? Apr 27, 2025 am 12:03 AM

Pythonlistsarepartofthestandardlibrary,whilearraysarenot.Listsarebuilt-in,versatile,andusedforstoringcollections,whereasarraysareprovidedbythearraymoduleandlesscommonlyusedduetolimitedfunctionality.

Python: Automation, Scripting, and Task Management Python: Automation, Scripting, and Task Management Apr 16, 2025 am 12:14 AM

Python excels in automation, scripting, and task management. 1) Automation: File backup is realized through standard libraries such as os and shutil. 2) Script writing: Use the psutil library to monitor system resources. 3) Task management: Use the schedule library to schedule tasks. Python's ease of use and rich library support makes it the preferred tool in these areas.

Learning Python: Is 2 Hours of Daily Study Sufficient? Learning Python: Is 2 Hours of Daily Study Sufficient? Apr 18, 2025 am 12:22 AM

Is it enough to learn Python for two hours a day? It depends on your goals and learning methods. 1) Develop a clear learning plan, 2) Select appropriate learning resources and methods, 3) Practice and review and consolidate hands-on practice and review and consolidate, and you can gradually master the basic knowledge and advanced functions of Python during this period.

Python vs. C  : Understanding the Key Differences Python vs. C : Understanding the Key Differences Apr 21, 2025 am 12:18 AM

Python and C each have their own advantages, and the choice should be based on project requirements. 1) Python is suitable for rapid development and data processing due to its concise syntax and dynamic typing. 2)C is suitable for high performance and system programming due to its static typing and manual memory management.

See all articles