Simple plateformer game playing

This commit is contained in:
2026-10-10 13:38:11 +02:00
commit 5cc2f19887
17 changed files with 531 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
.venv
venv
__pycache__/
+15
View File
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Main.py",
"type": "debugpy",
"request": "launch",
"program": "${cwd}/main.py",
"console": "integratedTerminal"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
from .stoneBlock import StoneBlock
from .spawnPointBlock import SpawnPointBlock
from .finishBlock import FinishBlock
BLOCKS = [
StoneBlock,
SpawnPointBlock,
FinishBlock,
]
+31
View File
@@ -0,0 +1,31 @@
from abc import abstractmethod
from game.gameState import GameState
class Block:
@property
@abstractmethod
def character(self):
pass
@property
@abstractmethod
def type(self):
pass
@property
@abstractmethod
def solid(self):
pass
def __init__(self):
pass
@abstractmethod
def on_collision(self, gameState: GameState):
pass
@abstractmethod
def to_string(self):
pass
+23
View File
@@ -0,0 +1,23 @@
from game.blocks.block import Block
from game.gameState import GameState
class FinishBlock(Block):
@property
def character(self):
return 'E'
@property
def type(self):
return 'finish'
@property
def solid(self):
return False
def on_collision(self, gameState: GameState):
gameState.set_stop_game(True)
gameState.set_game_won(True)
def to_string(self):
return "🏁"
+22
View File
@@ -0,0 +1,22 @@
from game.blocks.block import Block
from game.gameState import GameState
class SpawnPointBlock(Block):
@property
def character(self):
return 'S'
@property
def type(self):
return 'spawn_point'
@property
def solid(self):
return False
def on_collision(self, gameState: GameState):
pass
def to_string(self):
return "🚩"
+23
View File
@@ -0,0 +1,23 @@
from game.blocks.block import Block
from game.gameState import GameState
class StoneBlock(Block):
@property
def character(self):
return '#'
@property
def type(self):
return 'stone'
@property
def solid(self):
return True
def on_collision(self, gameState: GameState):
# Stone blocks are solid, so the player cannot move through them.
pass
def to_string(self):
return "⬛"
+145
View File
@@ -0,0 +1,145 @@
"""
Represents the main game class.
This class is responsible for managing the game's logic.
It offers to set a callback function that will be called when waiting for a player input.
It provides the gameState object to access the current state of the game.
It provides the Map object to access the map.
It provides some stopGame conditions to stop the game like the max number of moves or the player score.
It provides public methods to start, stop and reset the game.
It provides public methods to change the current map.
"""
from game.blocks.block import Block
from game.gameState import GameState
from game.map import Map
from colorama import Style
class Game:
def __init__(self, map: Map, gameState: GameState):
self.map = map
self.gameState = gameState
self.waitingForInputCallback = None
def set_waiting_for_input_callback(self, callback):
self.waitingForInputCallback = callback
def set_position_update_callback(self, callback):
self.positionUpdateCallback = callback
def _position_update(self):
if self.positionUpdateCallback:
self.positionUpdateCallback()
def start_game(self):
self.gameState.set_stop_game(False)
# Spawn the player at the spawn point
spawn_point_position = self.map.get_first_block_type_position('spawn_point')
if spawn_point_position is None:
raise ValueError("No spawn point found in the map.")
self.gameState.set_player_position(spawn_point_position)
while not self.gameState.stopGame:
self._position_update() # Update the screen for example
if self.gameState.playerVelocity == -1 and self.waitingForInputCallback:
self.gameState.set_player_velocity(self.waitingForInputCallback())
self.gameState.add_move_count()
# Try to move the player according to his velocity
block_at_velocity = self._get_block_at_velocity()
if not block_at_velocity or not block_at_velocity.solid:
velocity_position = self.get_position_at_velocity() # TODO : optimize
if velocity_position:
self.gameState.set_player_position(velocity_position)
if block_at_velocity:
block_at_velocity.on_collision(self.gameState) # Do something when the player is on the block, like add score or stop the game
# Check if the player is on a solid block
player_position = self.gameState.get_player_position()
block_at_player_position = self.map.get_block_at(player_position[0], player_position[1] + 1)
if block_at_player_position and block_at_player_position.solid:
self.gameState.set_player_velocity(-1) # Stop the player from falling
self._update_player_velocity()
def _update_player_velocity(self):
current_velocity = self.gameState.playerVelocity
if current_velocity == -1: # If no velocity, then the player is not moving, so we don't need to update the velocity
return
if current_velocity == "down": # Falling straight down → Still falling except if the player is on a block
if not self._get_block_at_velocity() == None and self._get_block_at_velocity().solid: # If block at where the player is going is solid, then the player will stop falling
self.gameState.set_player_velocity(-1)
return
if current_velocity is "up": # Going up → Going down
self.gameState.set_player_velocity("down")
return
if current_velocity in ["left_down", "right_down"]:
self.gameState.set_player_velocity("down")
return
if current_velocity in ["left", "right"]:
self.gameState.set_player_velocity(current_velocity + "_down")
block_at_velocity = self._get_block_at_velocity()
if not block_at_velocity == None and block_at_velocity.solid: # If block at where the player is going is solid, then the player will fall down
self.gameState.set_player_velocity("down")
return
if current_velocity in ["left_up", "right_up"]:
self.gameState.set_player_velocity(current_velocity.replace("_up", "_down"))
block_at_velocity = self._get_block_at_velocity()
if not block_at_velocity == None and block_at_velocity.solid: # If block at where the player is going is solid, then the player will fall down
self.gameState.set_player_velocity("down")
return
def get_position_at_velocity(self) -> tuple | None:
player_position = self.gameState.get_player_position()
current_velocity = self.gameState.playerVelocity
if current_velocity == -1:
return None
if current_velocity == "down":
return (player_position[0], player_position[1] + 1)
if current_velocity == "left_down":
return (player_position[0] - 1, player_position[1] + 1)
if current_velocity == "right_down":
return (player_position[0] + 1, player_position[1] + 1)
if current_velocity == "left":
return (player_position[0] - 1, player_position[1])
if current_velocity == "right":
return (player_position[0] + 1, player_position[1])
if current_velocity == "left_up":
return (player_position[0] - 1, player_position[1] - 1)
if current_velocity == "right_up":
return (player_position[0] + 1, player_position[1] - 1)
return None
def _get_block_at_velocity(self) -> Block | None:
position_at_velocity = self.get_position_at_velocity()
if position_at_velocity is None:
return None
return self.map.get_block_at(position_at_velocity[0], position_at_velocity[1])
def stop_game(self):
self.gameState.set_stop_game(True)
def to_string(self):
"""
Returns a string representation of the game state and map with player position.
"""
player_position = self.gameState.get_player_position()
map_string_dict = self.map.to_string_dict()
map_string_dict[player_position[1]][player_position[0]] = f'{Style.BRIGHT}🦧'
return '\n'.join([''.join(character + Style.RESET_ALL for character in row) for row in map_string_dict])
+46
View File
@@ -0,0 +1,46 @@
"""
Represents the current state of the game.
Contains :
- Player score
- stopGame flag
- Player position
"""
class GameState:
PLAYER_MOVEMENTS = ["left_down", "left", "left_up", "up", "right_up", "right", "right_down"]
PLAYER_VELOCITIES = [*PLAYER_MOVEMENTS, "down"]
def __init__(self):
self.score = 0
self.playerPosition = (0, 0)
self.stopGame = False
self.movesCount = 0
self.playerVelocity = -1 # -1 means no velocity
self.gameWon = False
def add_score(self, points):
self.score += points
def set_player_position(self, x, y):
self.playerPosition = (x, y)
def set_player_position(self, position: tuple):
self.playerPosition = position
def get_player_position(self):
return self.playerPosition
def set_player_velocity(self, velocity):
"""Set the player velocity, meaning where the player is currently going
"""
if velocity not in self.PLAYER_VELOCITIES and velocity != -1:
raise ValueError(f"Invalid player velocity: {velocity}. Must be one of {self.PLAYER_VELOCITIES} or -1.")
self.playerVelocity = velocity
def add_move_count(self, nb=1):
self.movesCount += nb
def set_stop_game(self, stop):
self.stopGame = stop
def set_game_won(self, won):
self.gameWon = won
+5
View File
@@ -0,0 +1,5 @@
from .userPlayerGameHandler import UserPlayerGameHandler
GAME_HANDLERS= {
"user player": UserPlayerGameHandler,
}
+17
View File
@@ -0,0 +1,17 @@
from abc import abstractmethod
from game.game import Game
class GameHandler:
def __init__(self, game: Game):
self.game = game
self.game.set_waiting_for_input_callback(self.on_input_request)
self.game.set_position_update_callback(self.on_position_update)
@abstractmethod
def on_position_update(self) -> None:
pass
@abstractmethod
def on_input_request(self) -> str:
pass
+24
View File
@@ -0,0 +1,24 @@
from time import sleep
from game.game import Game
from game.handlers.gameHandler import GameHandler
class UserPlayerGameHandler(GameHandler):
KEYBOARD_INPUTS = ["w", "q", "a", "z", "e", "d", "c"]
def __init__(self, game: Game):
super().__init__(game)
def on_position_update(self) -> None:
"""
Refresh the screen
"""
print("\033[2J\033[H" + self.game.to_string())
sleep(0.1)
def on_input_request(self) -> str:
"""
Listen for keyboard input, and map it to a gameState PLAYER_MOVEMENTS
"""
return self.game.gameState.PLAYER_MOVEMENTS[self.KEYBOARD_INPUTS.index(input(f"Enter a movement ({', '.join(self.KEYBOARD_INPUTS)}): "))]
+54
View File
@@ -0,0 +1,54 @@
"""
Represent a map in the game.
Contains the map data :
- Map name
- Map width and height
- Map difficulty
- The map layout (2D array of blocks)
"""
from game.blocks.block import Block
class Map:
def __init__(self, map_data: dict):
self.name = map_data.get('name', 'Unknown')
self.width = int(map_data.get('width', 0))
self.height = int(map_data.get('height', 0))
self.difficulty = float(map_data.get('difficulty', 0.0))
self.layout = map_data.get('layout', [])
def get_block_at(self, x, y) -> Block | None:
if 0 <= x < self.width and 0 <= y < self.height:
return self.layout[y][x]
return None
def get_first_block_type_position(self, block_type):
for y in range(self.height):
for x in range(self.width):
block = self.get_block_at(x, y)
if block and block.type == block_type:
return (x, y)
return None
def get_block_type_at_position(self, position):
x, y = position
block = self.get_block_at(x, y)
if block:
return block.type
return None
def to_string_dict(self):
"""
Returns a string matrix representation of the map layout.
"""
string_array = []
for row in self.layout:
string_array_row = []
for block in row:
if block is None:
string_array_row.append(" ")
else:
string_array_row.append(block.to_string())
string_array.append(string_array_row)
return string_array
+60
View File
@@ -0,0 +1,60 @@
"""
Reads map files and returns a Map object.
It provides a method to read a map file by it's name and return the corresponding Map object.
It provides a method to get all of the map names in the maps directory.
"""
import os
from game.map import Map
import tomllib
class MapReader:
DIRECTORY = "maps/"
def __init__(self):
pass
def get_map_names(self):
return [f[:-5] for f in os.listdir(self.DIRECTORY) if f.endswith('.toml')]
def read_map(self, map_name):
import configparser
map_path = os.path.join(self.DIRECTORY, map_name + ".toml")
if not os.path.exists(map_path):
raise FileNotFoundError(f"Map file {map_name} not found in {self.DIRECTORY}")
with open(map_path, 'rb') as f:
config = tomllib.load(f)
map_data = {
'name': map_name,
'width': config.get('width', 0),
'height': config.get('height', 0),
'difficulty': config.get('difficulty', 0.0),
'layout': self._parse_layout(config.get('layout', ''))
}
return Map(map_data)
def _get_block_characters_mapping(self):
# Returns a dictionary mapping block characters to their corresponding block classes.
from game.blocks import BLOCKS
return {block().character: block for block in BLOCKS}
def _parse_layout(self, mapString):
# Takes the blocks and translates their characters into the corresponding block objects.
block_characters_mapping = self._get_block_characters_mapping()
layout = []
for line in mapString.strip().splitlines():
row = []
for char in line:
block_class = block_characters_mapping.get(char)
if block_class:
row.append(block_class())
else:
row.append(None)
layout.append(row)
return layout
+26
View File
@@ -0,0 +1,26 @@
"""
Represents a set of maps in the game. At initialization, it loads the desired maps.
It provides a method to get a number of maps with the difficulty level as close as possible to the desired difficulty level.
"""
from game.mapReader import MapReader
class MapSet:
def __init__(self, mapReader: MapReader, mapNames: list):
self.mapReader = mapReader
self.maps = [self.mapReader.read_map(name) for name in mapNames]
def get_map(self, index: int):
if index < 0 or index >= len(self.maps):
raise IndexError("Map index out of range.")
return self.maps[index]
def get_maps_by_difficulty(self, desired_difficulty: int, num_maps: int):
"""
Returns a list of maps with difficulty levels as close as possible to the desired difficulty level.
If there are not enough maps, it returns all available maps.
"""
if len(self.maps) == 0:
raise ValueError("No maps available in the MapSet.")
sorted_maps = sorted(self.maps, key=lambda m: abs(m.difficulty - desired_difficulty))
return sorted_maps[:num_maps]
+17
View File
@@ -0,0 +1,17 @@
from game.game import Game
from game.gameState import GameState
from game.handlers import GAME_HANDLERS
from game.map import Map
from game.mapReader import MapReader
from game.mapSet import MapSet
if __name__ == "__main__":
mapReader = MapReader()
mapSet = MapSet(mapReader, mapReader.get_map_names())
map = mapSet.get_maps_by_difficulty(0, 1)[0]
gameState = GameState()
game = Game(map, gameState)
gameHandler = GAME_HANDLERS["user player"](game)
game.start_game()
# run_game()
+11
View File
@@ -0,0 +1,11 @@
width = 12
height = 6
difficulty = 10
layout = """\
############
# E#
# ° °##
# ° # ###
#S # ##^###
############
"""