Simple plateformer game playing
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.venv
|
||||||
|
venv
|
||||||
|
__pycache__/
|
||||||
Vendored
+15
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from .stoneBlock import StoneBlock
|
||||||
|
from .spawnPointBlock import SpawnPointBlock
|
||||||
|
from .finishBlock import FinishBlock
|
||||||
|
|
||||||
|
BLOCKS = [
|
||||||
|
StoneBlock,
|
||||||
|
SpawnPointBlock,
|
||||||
|
FinishBlock,
|
||||||
|
]
|
||||||
@@ -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
|
||||||
@@ -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 "🏁"
|
||||||
@@ -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 "🚩"
|
||||||
@@ -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
@@ -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])
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .userPlayerGameHandler import UserPlayerGameHandler
|
||||||
|
|
||||||
|
GAME_HANDLERS= {
|
||||||
|
"user player": UserPlayerGameHandler,
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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]
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
width = 12
|
||||||
|
height = 6
|
||||||
|
difficulty = 10
|
||||||
|
layout = """\
|
||||||
|
############
|
||||||
|
# E#
|
||||||
|
# ° °##
|
||||||
|
# ° # ###
|
||||||
|
#S # ##^###
|
||||||
|
############
|
||||||
|
"""
|
||||||
Reference in New Issue
Block a user