Compare commits
21 Commits
d37214f1ed
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d6e2fc7be9 | |||
| 3d2f98d56e | |||
| 0fd490b97c | |||
| 104297e88b | |||
| 104c6fac63 | |||
| a7a3d973a3 | |||
| f56b8c1e9c | |||
| ce720a92b2 | |||
| a3fdce7df4 | |||
| d7e1711103 | |||
| 5a78bb3b9d | |||
| e8d04bfdc3 | |||
| 84d5bd9e4d | |||
| 2489138167 | |||
| 7553c293ff | |||
| c891b4d6f4 | |||
| 7d3432538e | |||
| 17f00ebca2 | |||
| 14a39b7154 | |||
| 10a3d93821 | |||
| 9030146ec8 |
@@ -0,0 +1,9 @@
|
||||
# show extra spaces after certains emojis to align them in the terminal
|
||||
SHOW_EXTRA_EMOJI_SPACES=true
|
||||
|
||||
# === AI HTTP SERVER ===
|
||||
# The host and port for the AI HTTP server.
|
||||
AI_HOST=127.0.0.1
|
||||
AI_PORT=8765
|
||||
# Number of workers for the AI HTTP server. This should be set to the number of CPU cores available on the machine.
|
||||
AI_MAX_WORKERS=10
|
||||
@@ -1,3 +1,5 @@
|
||||
.venv
|
||||
venv
|
||||
__pycache__/
|
||||
|
||||
.env
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
# AI TCP API
|
||||
|
||||
The AI handler listens on `127.0.0.1:8765` by default. Set `AI_HOST`,
|
||||
`AI_PORT`, or `AI_MAX_WORKERS` to change the server configuration. Messages
|
||||
are UTF-8 JSON objects separated by newlines. The connection stays open for
|
||||
multiple requests.
|
||||
|
||||
## Ping
|
||||
|
||||
Request: `{"type":"ping"}`
|
||||
|
||||
Response: `{"ok":true,"type":"pong"}`
|
||||
|
||||
## Start a simulation
|
||||
|
||||
The server chooses the map. For the first training simulation it selects the
|
||||
map closest to difficulty 0. After a simulation finishes, the server computes
|
||||
each model's score from coins, progress toward the finish, and whether it won.
|
||||
The best score becomes the next training difficulty. Validation requests ignore
|
||||
the training score and randomly select a map with difficulty at least 70, or
|
||||
any map when no such map exists.
|
||||
|
||||
The score is from 0 to 100:
|
||||
|
||||
`50 * coin_progress + 10 * distance_progress + 40 * won`
|
||||
|
||||
where `coin_progress` is collected coins divided by the map's total coins,
|
||||
`distance_progress` is progress from the spawn point to the finish, and `won`
|
||||
is 1 when the finish was reached and 0 when the model stopped elsewhere,
|
||||
including on spikes.
|
||||
|
||||
Send one model entry per model. The model entries contain only an ID; moves
|
||||
are requested separately on every turn.
|
||||
|
||||
```json
|
||||
{"type":"simulate","validation":false,"models":[{"id":"model-a"},{"id":"model-b"}]}
|
||||
```
|
||||
|
||||
Every model gets a separate `Game` instance and can run in parallel. Valid
|
||||
moves are `left_down`, `left`, `left_up`, `up`, `right_up`,
|
||||
`right`, and `right_down`. Send `null` for a model when it has no more moves;
|
||||
that model is stopped for the current generation.
|
||||
|
||||
The initial response is a `move_request`. It contains the selected map and the
|
||||
initial state of every model. The map is sent only in this initial response;
|
||||
clients should cache it for the rest of the simulation.
|
||||
|
||||
```json
|
||||
{
|
||||
"ok":true,
|
||||
"type":"move_request",
|
||||
"map":{"name":"default","difficulty":30.0,"layout":[["stone", "stone"]]},
|
||||
"results":{"model-a":{"won":false,"coins":0,"moves":0,"position":[1,4],"finished":false,"changes":[]},"model-b":{"won":false,"coins":0,"moves":0,"position":[1,4],"finished":false,"changes":[]}}
|
||||
}
|
||||
```
|
||||
|
||||
After receiving this response, send one move for every model:
|
||||
|
||||
```json
|
||||
{"type":"step","moves":{"model-a":"right","model-b":"right_up"}}
|
||||
```
|
||||
|
||||
## Advance one turn
|
||||
|
||||
The client must send exactly one move for every model on each turn. The server
|
||||
advances all model simulations in parallel and replies before the client must
|
||||
predict the next moves.
|
||||
|
||||
```json
|
||||
{"type":"step","moves":{"model-a":"right","model-b":null}}
|
||||
```
|
||||
|
||||
The response is another `move_request` containing the updated state and score
|
||||
for every model. Each model also receives `changes`, a cumulative list of
|
||||
blocks that differ from the base map. For example, a collected coin is sent as
|
||||
`{"position":[4,7],"type":null}`. Unchanged turns return an empty list. It
|
||||
asks the client for the next move of every unfinished model. A model that has
|
||||
no more moves can send `null`:
|
||||
|
||||
```json
|
||||
{"type":"step","moves":{"model-a":"right","model-b":null}}
|
||||
```
|
||||
|
||||
When every model has finished, the response type becomes `simulation_result`.
|
||||
It contains the final state and score for every model, plus the best score used
|
||||
to choose the next training map:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok":true,
|
||||
"type":"simulation_result",
|
||||
"results":{
|
||||
"model-a":{
|
||||
"won":true,
|
||||
"coins":3,
|
||||
"moves":18,
|
||||
"position":[23,1],
|
||||
"finished":true,
|
||||
"changes":[{"position":[4,7],"type":null}],
|
||||
"score":92
|
||||
},
|
||||
"model-b":{
|
||||
"won":false,
|
||||
"coins":1,
|
||||
"moves":7,
|
||||
"position":[8,4],
|
||||
"finished":true,
|
||||
"changes":[],
|
||||
"score":24
|
||||
}
|
||||
},
|
||||
"score":92
|
||||
}
|
||||
```
|
||||
|
||||
The score is calculated by the server using the weights described above. The
|
||||
top-level `score` is the best model score for the completed generation.
|
||||
Malformed requests receive `{"ok":false,"type":"error","error":"..."}`.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Améliorations
|
||||
|
||||
- Pénaliser les mouvements impossibles : intéressant, même pour l'algo génétique, pourrait optimiser le genome
|
||||
- Pénaliser le fait de terminer la simulation sur une case où l'on est déjà passé ou pénaliser de revenir sur une case où l'on est passé
|
||||
@@ -1,9 +1,13 @@
|
||||
from .stoneBlock import StoneBlock
|
||||
from .spawnPointBlock import SpawnPointBlock
|
||||
from .finishBlock import FinishBlock
|
||||
from .coinBlock import CoinBlock
|
||||
from .spikesBlock import SpikesBlock
|
||||
|
||||
BLOCKS = [
|
||||
StoneBlock,
|
||||
SpawnPointBlock,
|
||||
FinishBlock,
|
||||
CoinBlock,
|
||||
SpikesBlock,
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
|
||||
from game.blocks.block import Block
|
||||
from game.gameState import GameState
|
||||
|
||||
class CoinBlock(Block):
|
||||
def __init__(self):
|
||||
self.collected = False
|
||||
|
||||
@property
|
||||
def character(self):
|
||||
return '°'
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'coin'
|
||||
|
||||
@property
|
||||
def solid(self):
|
||||
return False
|
||||
|
||||
def on_collision(self, gameState: GameState):
|
||||
if not self.collected:
|
||||
gameState.add_coin()
|
||||
self.collected = True
|
||||
|
||||
def to_string(self):
|
||||
return " " if self.collected else "🪙" + (" " if os.getenv("SHOW_EXTRA_EMOJI_SPACES", "false").lower() == "true" else "")
|
||||
@@ -16,8 +16,8 @@ class FinishBlock(Block):
|
||||
return False
|
||||
|
||||
def on_collision(self, gameState: GameState):
|
||||
gameState.set_stop_game(True)
|
||||
gameState.set_game_won(True)
|
||||
gameState.set_stop_game(True)
|
||||
|
||||
def to_string(self):
|
||||
return "🏁"
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
|
||||
from game.blocks.block import Block
|
||||
from game.gameState import GameState
|
||||
|
||||
class SpikesBlock(Block):
|
||||
@property
|
||||
def character(self):
|
||||
return '^'
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return 'spikes'
|
||||
|
||||
@property
|
||||
def solid(self):
|
||||
return False
|
||||
|
||||
def on_collision(self, gameState: GameState):
|
||||
gameState.set_game_won(False)
|
||||
gameState.set_stop_game(True)
|
||||
|
||||
def to_string(self):
|
||||
return "🔥"
|
||||
+47
-25
@@ -4,7 +4,7 @@ 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.
|
||||
TODO: It provides some stopGame conditions to stop the game like the max number of moves.
|
||||
It provides public methods to start, stop and reset the game.
|
||||
It provides public methods to change the current map.
|
||||
"""
|
||||
@@ -20,41 +20,60 @@ class Game:
|
||||
self.map = map
|
||||
self.gameState = gameState
|
||||
self.waitingForInputCallback = None
|
||||
self.positionUpdateCallback = None
|
||||
self.gameStopCallback = None
|
||||
|
||||
def set_waiting_for_input_callback(self, callback):
|
||||
def set_waiting_for_input_callback(self, callback: callable):
|
||||
self.waitingForInputCallback = callback
|
||||
|
||||
def set_position_update_callback(self, callback):
|
||||
def set_position_update_callback(self, callback: callable):
|
||||
self.positionUpdateCallback = callback
|
||||
|
||||
def _position_update(self):
|
||||
if self.positionUpdateCallback:
|
||||
self.positionUpdateCallback()
|
||||
|
||||
def set_game_stop_callback(self, callback: callable):
|
||||
self.gameStopCallback = callback
|
||||
|
||||
def start_game(self):
|
||||
self.gameState.set_stop_game(False)
|
||||
falling_steps = 0
|
||||
|
||||
# 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)
|
||||
self._position_update()
|
||||
|
||||
while not self.gameState.stopGame:
|
||||
self._position_update() # Update the screen for example
|
||||
|
||||
if self.gameState.playerVelocity == -1 and self.waitingForInputCallback:
|
||||
falling_steps = 0
|
||||
self.gameState.set_player_velocity(self.waitingForInputCallback())
|
||||
self.gameState.add_move_count()
|
||||
elif self.gameState.playerVelocity != -1:
|
||||
falling_steps += 1
|
||||
if falling_steps > self.map.height:
|
||||
self.gameState.set_game_won(False)
|
||||
self.gameState.set_stop_game(True)
|
||||
self._position_update()
|
||||
continue
|
||||
|
||||
# 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
|
||||
velocity_position = self.get_position_at_velocity()
|
||||
if velocity_position is not None and not self.map.is_position_in_bounds(velocity_position):
|
||||
self.gameState.set_stop_game(True)
|
||||
self._position_update()
|
||||
continue
|
||||
block_at_velocity = None
|
||||
if velocity_position is not None:
|
||||
block_at_velocity = self.map.get_block_at(velocity_position[0], velocity_position[1])
|
||||
if not block_at_velocity or not block_at_velocity.solid: # Empty space or non-solid block → Move the player to the new position
|
||||
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
|
||||
block_at_velocity.on_collision(self.gameState) # Do something when the player is on the block, like add coins 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)
|
||||
@@ -63,6 +82,11 @@ class Game:
|
||||
|
||||
self._update_player_velocity()
|
||||
|
||||
self._position_update() # Update the screen for example
|
||||
|
||||
if self.gameStopCallback and self.gameStopCallback(self.gameState):
|
||||
self.gameState.set_stop_game(True)
|
||||
break
|
||||
|
||||
def _update_player_velocity(self):
|
||||
current_velocity = self.gameState.playerVelocity
|
||||
@@ -74,7 +98,7 @@ class Game:
|
||||
self.gameState.set_player_velocity(-1)
|
||||
return
|
||||
|
||||
if current_velocity is "up": # Going up → Going down
|
||||
if current_velocity == "up": # Going up → Going down
|
||||
self.gameState.set_player_velocity("down")
|
||||
return
|
||||
|
||||
@@ -100,29 +124,27 @@ class Game:
|
||||
player_position = self.gameState.get_player_position()
|
||||
current_velocity = self.gameState.playerVelocity
|
||||
|
||||
if current_velocity == -1:
|
||||
match current_velocity:
|
||||
case -1:
|
||||
return None
|
||||
|
||||
if current_velocity == "down":
|
||||
case "down":
|
||||
return (player_position[0], player_position[1] + 1)
|
||||
|
||||
if current_velocity == "left_down":
|
||||
case "up":
|
||||
return (player_position[0], player_position[1] - 1)
|
||||
case "left_down":
|
||||
return (player_position[0] - 1, player_position[1] + 1)
|
||||
|
||||
if current_velocity == "right_down":
|
||||
case "right_down":
|
||||
return (player_position[0] + 1, player_position[1] + 1)
|
||||
|
||||
if current_velocity == "left":
|
||||
case "left":
|
||||
return (player_position[0] - 1, player_position[1])
|
||||
|
||||
if current_velocity == "right":
|
||||
case "right":
|
||||
return (player_position[0] + 1, player_position[1])
|
||||
|
||||
if current_velocity == "left_up":
|
||||
case "left_up":
|
||||
return (player_position[0] - 1, player_position[1] - 1)
|
||||
|
||||
if current_velocity == "right_up":
|
||||
case "right_up":
|
||||
return (player_position[0] + 1, player_position[1] - 1)
|
||||
case _:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Represents the current state of the game.
|
||||
Contains :
|
||||
- Player score
|
||||
- Player coins
|
||||
- stopGame flag
|
||||
- Player position
|
||||
"""
|
||||
@@ -10,15 +10,12 @@ class GameState:
|
||||
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
|
||||
self.coins = 0
|
||||
|
||||
def set_player_position(self, x, y):
|
||||
self.playerPosition = (x, y)
|
||||
@@ -44,3 +41,6 @@ class GameState:
|
||||
|
||||
def set_game_won(self, won):
|
||||
self.gameWon = won
|
||||
|
||||
def add_coin(self):
|
||||
self.coins += 1
|
||||
@@ -1,5 +1,7 @@
|
||||
from .userPlayerGameHandler import UserPlayerGameHandler
|
||||
from .aiHttpGameHandler import AiHttpGameHandler
|
||||
|
||||
GAME_HANDLERS= {
|
||||
"user player": UserPlayerGameHandler,
|
||||
"ai tcp": AiHttpGameHandler,
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
"""
|
||||
AI HTTP server interface for AI training
|
||||
|
||||
Opens a TCP socket to listen for incoming connections from an AI client.
|
||||
When a new connection is established, the server intialise a new training session :
|
||||
- Model Score is set to 0
|
||||
For the duration of the training session, two mapSets are created, one with hard maps and one with all of the other maps.
|
||||
When the AI client sends a request for a new game or validation game,
|
||||
the server loads a map based on the saved best score or from the hard mapSet if a validation game is requested,
|
||||
and sends it to the client as a matrix of block types strings.
|
||||
The client then sends back a list of moves to be executed by the game engine. E
|
||||
Each item of the list is a different model givin a string representing a move from GameState.PLAYER_MOVEMENTS to execute in the simulation.
|
||||
The server then executes the moves in the game engine for each model
|
||||
and sends back the new player position or game scores
|
||||
(calculated here using formula with weights) if the game is finished to the client.
|
||||
This process is repeated until all models have finished the game.
|
||||
The client will then train the model based on their scores
|
||||
and the server set the Model Score to the best score of the models
|
||||
and shows in a separate thread the best model series of moves on the previous map.
|
||||
The client then sends a new request for a new game and the process repeats.
|
||||
The simulation runs in a pool of threads to allow multiple models to be trained in parallel.
|
||||
The handler makes sure an AI client is not stuck and only allows a maximum number
|
||||
of moves to be executed in a single game simulation, based on the map min_moves value.
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
import socketserver
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from copy import deepcopy
|
||||
from threading import Condition, Thread
|
||||
from time import sleep
|
||||
|
||||
from game.handlers.aiSimulation import AiSimulation
|
||||
from game.game import Game
|
||||
from game.gameState import GameState
|
||||
from game.handlers.gameHandler import GameHandler
|
||||
from game.map import Map
|
||||
from game.mapReader import MapReader
|
||||
from game.mapSet import MapSet
|
||||
|
||||
class AiHttpGameHandler(GameHandler):
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
DEFAULT_PORT = 8765
|
||||
VALIDATION_DIFFICULTY = 90
|
||||
REPLAY_FRAME_DELAY = 0.1
|
||||
# Scoring weights TOTAL MUST BE 100
|
||||
COINS_WEIGHT = 50
|
||||
STEPS_WEIGHT = 20
|
||||
DISTANCE_WEIGHT = 10
|
||||
WIN_WEIGHT = 20
|
||||
PLAY_ALL_MAPS_PER_GENERATION = False # Are all maps played during a generation or only MAPS_PER_GENERATION maps per generation are played
|
||||
MAPS_PER_GENERATION = 3 # Evaluate across x maps per generation. Only used if PLAY_ALL_MAPS_PER_GENERATION is False. If True, all maps are played in a generation.
|
||||
MAP_PASS_MIN_SCORE = 20 # Minimum score to consider a map passed. Only used if PLAY_ALL_MAPS_PER_GENERATION is False. If True, all maps are played in a generation.
|
||||
MAP_DIFFICULTY_VARIANCE = 5 # Lower value will make the map selection more variable, higher value will make the map selection more consistent with the current performance. Only used if PLAY_ALL_MAPS_PER_GENERATION is False. A value too high may produce no evolving behavior
|
||||
CONSISTENCY_PENALTY_WEIGHT = 0.4 # Penalty for inconsistent map scores
|
||||
MINIMUM_PERFORMANCE_DELTA = 0.05 # Minimum performance delta to add every generation. This ensures that the map will always change even if the ai is stuck in a local optimum. Only used if PLAY_ALL_MAPS_PER_GENERATION is False. A value too high may produce no evolving behavior
|
||||
|
||||
def __init__(self, host: str | None = None, port: int | None = None,
|
||||
max_workers: int | None = None):
|
||||
super().__init__()
|
||||
self.host = host or os.getenv("AI_HOST", self.DEFAULT_HOST)
|
||||
self.port = port or int(os.getenv("AI_PORT", self.DEFAULT_PORT))
|
||||
self.max_workers = max_workers or int(os.getenv("AI_MAX_WORKERS", "4"))
|
||||
self.map_reader = MapReader()
|
||||
self.map_set = MapSet(self.map_reader, self.map_reader.get_map_names())
|
||||
self.validation_maps = [
|
||||
game_map for game_map in self.map_set.maps
|
||||
if game_map.difficulty >= self.VALIDATION_DIFFICULTY
|
||||
] or list(self.map_set.maps)
|
||||
|
||||
if (self.COINS_WEIGHT + self.DISTANCE_WEIGHT + self.STEPS_WEIGHT + self.WIN_WEIGHT) != 100:
|
||||
raise ValueError("Scoring weights must sum to 100.")
|
||||
|
||||
def run(self) -> None:
|
||||
handler = self
|
||||
|
||||
class RequestHandler(socketserver.StreamRequestHandler):
|
||||
def handle(self) -> None:
|
||||
session = {
|
||||
"performance": 0.0,
|
||||
"simulations": None,
|
||||
"best_run": None,
|
||||
"replay_condition": Condition(),
|
||||
"client_connected": True,
|
||||
"stagnant_generations": 0,
|
||||
"map_round": 0,
|
||||
"cumulative_scores": {},
|
||||
"last_results": {}
|
||||
}
|
||||
replay_thread = Thread(target=handler._replay_best_runs, args=(session,))
|
||||
replay_thread.start()
|
||||
print(f"AI client connected: {self.client_address}", flush=True)
|
||||
try:
|
||||
for raw_request in self.rfile:
|
||||
try:
|
||||
request = json.loads(raw_request)
|
||||
response = handler._handle_request(request, session)
|
||||
except (json.JSONDecodeError, TypeError, ValueError, KeyError) as error:
|
||||
response = {"ok": False, "type": "error", "error": str(error)}
|
||||
compact_response = json.dumps(response, separators=(",", ":"))
|
||||
self.wfile.write((compact_response + "\n").encode("utf-8"))
|
||||
self.wfile.flush()
|
||||
finally:
|
||||
with session["replay_condition"]:
|
||||
session["client_connected"] = False
|
||||
session["replay_condition"].notify_all()
|
||||
replay_thread.join()
|
||||
print(f"AI client disconnected: {self.client_address}", flush=True)
|
||||
|
||||
class ThreadingTCPServer(socketserver.ThreadingTCPServer):
|
||||
allow_reuse_address = True
|
||||
|
||||
with ThreadingTCPServer((self.host, self.port), RequestHandler) as server:
|
||||
self.port = server.server_address[1]
|
||||
print(f"AI TCP server listening on {self.host}:{self.port}")
|
||||
server.serve_forever()
|
||||
|
||||
def _handle_request(self, request: dict, session: dict | None = None) -> dict:
|
||||
if not isinstance(request, dict):
|
||||
raise ValueError("Each request must be a JSON object.")
|
||||
request_type = request.get("type")
|
||||
if request_type == "ping":
|
||||
return {"ok": True, "type": "pong"}
|
||||
if request_type == "simulate":
|
||||
return self._simulate(request, session)
|
||||
if request_type == "step":
|
||||
return self._step(request, session)
|
||||
raise ValueError("Unknown request type. Expected 'ping', 'simulate', or 'step'.")
|
||||
|
||||
def _simulate(self, request: dict, session: dict | None = None) -> dict:
|
||||
models = request.get("models")
|
||||
if not isinstance(models, list) or not models:
|
||||
raise ValueError("'models' must be a non-empty list.")
|
||||
if not all(isinstance(model, dict) for model in models):
|
||||
raise ValueError("Each item in 'models' must be a JSON object.")
|
||||
validation = bool(request.get("validation", False))
|
||||
if session is None:
|
||||
raise ValueError("Simulation requests must belong to a TCP session.")
|
||||
if session.get("simulations"):
|
||||
raise ValueError("A simulation is already active in this session.")
|
||||
|
||||
# Initialize generation round tracking and select 3 maps upfront if starting fresh
|
||||
if session.get("map_round", 0) == 0:
|
||||
session["generation"] = session.get("generation", 0) + 1
|
||||
session["map_scores"] = {model.get("id"): [] for model in models}
|
||||
|
||||
# Pre-select the 3 maps for this entire generation based on current performance
|
||||
if validation:
|
||||
session["generation_maps"] = [
|
||||
random.choice(self.validation_maps)
|
||||
for _ in range(self.MAPS_PER_GENERATION)
|
||||
]
|
||||
else:
|
||||
perf = session.get("performance", 0.0)
|
||||
if self.PLAY_ALL_MAPS_PER_GENERATION:
|
||||
session["generation_maps"] = self.map_set.get_maps()
|
||||
else:
|
||||
session["generation_maps"] = sorted(self.map_set.get_maps_by_difficulty(round(perf), self.MAPS_PER_GENERATION), key=lambda m: m.difficulty)
|
||||
|
||||
# Fetch the pre-selected map for the current round
|
||||
map_round = session.get("map_round", 0)
|
||||
selected_map = session["generation_maps"][map_round]
|
||||
|
||||
session["last_result"] = None
|
||||
|
||||
simulations = {}
|
||||
for model in models:
|
||||
model_id = model.get("id")
|
||||
if not isinstance(model_id, str) or not model_id:
|
||||
raise ValueError("Each model must have a non-empty string 'id'.")
|
||||
if model_id in simulations:
|
||||
raise ValueError(f"Duplicate model id '{model_id}'.")
|
||||
simulation = AiSimulation(selected_map)
|
||||
simulation.start()
|
||||
simulations[model_id] = simulation
|
||||
|
||||
session["simulations"] = simulations
|
||||
session["map"] = selected_map
|
||||
return {
|
||||
"ok": True,
|
||||
"type": "move_request",
|
||||
"map": self._map_payload(selected_map),
|
||||
"results": self._results(simulations),
|
||||
}
|
||||
|
||||
def _step(self, request: dict, session: dict | None) -> dict:
|
||||
if session is None:
|
||||
raise ValueError("No simulation is active. Send a 'simulate' request first.")
|
||||
if not session.get("simulations"):
|
||||
if session.get("last_result") is not None:
|
||||
return session["last_result"]
|
||||
raise ValueError("No simulation is active. Send a 'simulate' request first.")
|
||||
moves = request.get("moves")
|
||||
# print(
|
||||
# "Step payload contains moves for: "
|
||||
# f"{list(moves) if isinstance(moves, dict) else type(moves).__name__}",
|
||||
# flush=True,
|
||||
# )
|
||||
if not isinstance(moves, dict):
|
||||
raise ValueError("'moves' must be an object mapping model ids to moves.")
|
||||
simulations = session["simulations"]
|
||||
if set(moves) != set(simulations):
|
||||
raise ValueError("'moves' must contain exactly one move for every model.")
|
||||
for model_id, move in moves.items():
|
||||
if move is not None and (not isinstance(move, str) or move not in GameState.PLAYER_MOVEMENTS):
|
||||
raise ValueError(f"Invalid move for model '{model_id}'.")
|
||||
# print("Dispatching step to simulation workers", flush=True)
|
||||
|
||||
def step_model(item):
|
||||
model_id, move = item
|
||||
simulation = simulations[model_id]
|
||||
# print(f"Starting step for model '{model_id}'", flush=True)
|
||||
result = simulation.step(move)
|
||||
result["score"] = self._score_simulation(simulation, result)
|
||||
# print(f"Finished step for model '{model_id}'", flush=True)
|
||||
return model_id, result
|
||||
|
||||
with ThreadPoolExecutor(max_workers=min(self.max_workers, len(simulations))) as executor:
|
||||
results = dict(executor.map(step_model, moves.items()))
|
||||
# print(f"Completed step for models: {list(moves)}", flush=True)
|
||||
all_finished = all(result["finished"] for result in results.values())
|
||||
response = {"ok": True, "type": "move_request", "results": results}
|
||||
|
||||
if all_finished:
|
||||
session["map_round"] += 1
|
||||
|
||||
# Accumulate scores for this map round
|
||||
for model_id, res in results.items():
|
||||
session["map_scores"][model_id].append(res["score"])
|
||||
|
||||
# Check if we have completed all maps for this generation
|
||||
if session["map_round"] < len(session["generation_maps"]):
|
||||
# More maps remaining in this generation! Tell client to simulate next map.
|
||||
response["type"] = "map_round_complete"
|
||||
response["map_round"] = session["map_round"]
|
||||
session["simulations"] = None
|
||||
session["last_result"] = response
|
||||
else:
|
||||
# All maps completed -> finalize generation results (Average scores)
|
||||
response["type"] = "simulation_result"
|
||||
final_results = {}
|
||||
|
||||
# Calculate final scores for each model based on their score on every map
|
||||
for model_id, scores in session["map_scores"].items():
|
||||
# Calculate Mean Squared Error from perfection (100)
|
||||
squared_errors = [(100.0 - score) ** 2 for score in scores]
|
||||
mse = sum(squared_errors) / len(squared_errors)
|
||||
rmse = math.sqrt(mse)
|
||||
|
||||
# Final score penalizes variance/drops sharply based on RMSE
|
||||
final_score = round(max(0.0, min(100.0, 100.0 - rmse)), 2)
|
||||
|
||||
# Attach final structural properties from the last results set
|
||||
final_results[model_id] = deepcopy(results[model_id])
|
||||
final_results[model_id]["score"] = final_score
|
||||
|
||||
response["results"] = final_results
|
||||
best_score = max(res["score"] for res in final_results.values())
|
||||
best_model_id = max(final_results, key=lambda mid: final_results[mid]["score"])
|
||||
response["score"] = best_score
|
||||
|
||||
# --- Performance Update ---
|
||||
# Average score of the 90th percentile of the final results
|
||||
sorted_scores = sorted((res["score"] for res in final_results.values()), reverse=True)
|
||||
top_90_count = max(1, round(len(sorted_scores) * 0.9))
|
||||
top_90_scores = sorted_scores[:top_90_count]
|
||||
avg_top_90_score = sum(top_90_scores) / len(top_90_scores)
|
||||
performance_delta = (avg_top_90_score - self.MAP_PASS_MIN_SCORE) / self.MAP_DIFFICULTY_VARIANCE
|
||||
performance_delta = max(self.MINIMUM_PERFORMANCE_DELTA, performance_delta)
|
||||
session["performance"] = max(0.0, session.get("performance", 0.0) + performance_delta)
|
||||
# print(f"Generation {session['generation']} completed on difficulty {map_difficulty}. Best score: {best_score:.2f}. Updated performance: {session['performance']:.2f}", flush=True)
|
||||
|
||||
# Reset round counter for the next generation
|
||||
session["map_round"] = 0
|
||||
session["last_result"] = response
|
||||
session["simulations"] = None
|
||||
session["best_run"] = (
|
||||
deepcopy(session["map"]),
|
||||
list(simulations[best_model_id].moves),
|
||||
session["generation"],
|
||||
session["performance"],
|
||||
)
|
||||
replay_condition = session.get("replay_condition")
|
||||
if replay_condition is not None:
|
||||
with replay_condition:
|
||||
replay_condition.notify_all()
|
||||
return response
|
||||
|
||||
def _replay_best_runs(self, session: dict) -> None:
|
||||
condition = session["replay_condition"]
|
||||
while True:
|
||||
with condition:
|
||||
condition.wait_for(
|
||||
lambda: session["best_run"] is not None
|
||||
or not session["client_connected"]
|
||||
)
|
||||
best_run = session["best_run"]
|
||||
session["best_run"] = None
|
||||
if best_run is None:
|
||||
return
|
||||
self._show_best_run(*best_run)
|
||||
|
||||
def _show_best_run(self, game_map, moves: list[str], generation: int, performance: float) -> None:
|
||||
game = Game(game_map, GameState())
|
||||
remaining_moves = iter(moves)
|
||||
|
||||
def on_input_request() -> str:
|
||||
try:
|
||||
return next(remaining_moves)
|
||||
except StopIteration:
|
||||
game.stop_game()
|
||||
return -1
|
||||
|
||||
def on_position_update() -> None:
|
||||
game_string = game.to_string()
|
||||
frame_lines = []
|
||||
for index, line in enumerate(game_string.splitlines()):
|
||||
if index == 0:
|
||||
line += "\tGeneration: " + str(generation)
|
||||
elif index == 1:
|
||||
line += "\tPerformance: " + str(round(performance, 2))
|
||||
elif index == 2:
|
||||
line += "\tCoins: " + str(game.gameState.coins)
|
||||
elif index == 3:
|
||||
line += "\tMoves: " + str(game.gameState.movesCount)
|
||||
elif index == 4:
|
||||
line += "\tMove: " + moves[game.gameState.movesCount - 1] if len(moves) >= game.gameState.movesCount else "\tMove: None"
|
||||
frame_lines.append(line)
|
||||
print("\033[2J\033[H\n\n\n" + "\n".join(frame_lines), flush=True)
|
||||
sleep(self.REPLAY_FRAME_DELAY)
|
||||
|
||||
game.set_waiting_for_input_callback(on_input_request)
|
||||
game.set_position_update_callback(on_position_update)
|
||||
game.start_game()
|
||||
|
||||
def _results(self, simulations: dict) -> dict:
|
||||
results = {}
|
||||
for model_id, simulation in simulations.items():
|
||||
result = simulation.result()
|
||||
result["score"] = self._score_simulation(simulation, result)
|
||||
results[model_id] = result
|
||||
return results
|
||||
|
||||
def _select_map(self, performance: float, validation: bool):
|
||||
if validation:
|
||||
return random.choice(self.validation_maps)
|
||||
maps = self.map_set.get_maps_by_difficulty(round(performance), 2)
|
||||
return random.choice(maps)
|
||||
|
||||
def _score_simulation(self, simulation: AiSimulation, result: dict) -> int:
|
||||
game_map = simulation.game.map
|
||||
raw_score = self._score_result(game_map, result)
|
||||
max_score = self._get_map_max_score(game_map)
|
||||
if max_score == 0:
|
||||
return 0
|
||||
percentage = (raw_score / max_score) * 100.0
|
||||
return round(max(0.0, min(100.0, percentage)))
|
||||
|
||||
def _get_map_max_score(self, game_map: Map) -> int:
|
||||
result = {}
|
||||
result["position"] = game_map.get_first_block_type_position("finish")
|
||||
result["coins"] = game_map.count_blocks_of_type("coin")
|
||||
result["won"] = True
|
||||
result["moves"] = game_map.min_moves
|
||||
return self._score_result(game_map, result)
|
||||
|
||||
def _score_result(self, game_map: Map, result: dict) -> int:
|
||||
finish = game_map.get_first_block_type_position("finish")
|
||||
spawn = game_map.get_first_block_type_position("spawn_point")
|
||||
total_coins = game_map.count_blocks_of_type("coin")
|
||||
|
||||
if finish is None or spawn is None:
|
||||
distance_progress = 0.0
|
||||
else:
|
||||
start_distance = abs(spawn[0] - finish[0]) + abs(spawn[1] - finish[1])
|
||||
remaining_distance = abs(result["position"][0] - finish[0]) + abs(result["position"][1] - finish[1])
|
||||
distance_progress = 1.0 if start_distance == 0 else max(
|
||||
0.0, min(1.0, 1.0 - remaining_distance / start_distance)
|
||||
)
|
||||
steps_progress = 0.0 if result["moves"] == 0 else min(1.0, 1.0 - result["moves"] / game_map.min_moves)
|
||||
|
||||
coin_progress = 0.0 if total_coins == 0 else min(1.0, result["coins"] / total_coins)
|
||||
return round(
|
||||
self.COINS_WEIGHT * coin_progress
|
||||
+ self.DISTANCE_WEIGHT * distance_progress
|
||||
+ self.STEPS_WEIGHT * steps_progress
|
||||
+ self.WIN_WEIGHT * result["won"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _map_payload(game_map: Map) -> dict:
|
||||
return {
|
||||
"name": game_map.name,
|
||||
"difficulty": game_map.difficulty,
|
||||
"layout": game_map.to_type_dict(),
|
||||
}
|
||||
|
||||
def on_position_update(self) -> None:
|
||||
pass
|
||||
|
||||
def on_input_request(self) -> str:
|
||||
raise RuntimeError("AI input is supplied one turn at a time in 'step' requests.")
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Run one isolated game simulation for the AI HTTP handler."""
|
||||
|
||||
from copy import deepcopy
|
||||
from threading import Condition, Thread
|
||||
from time import sleep
|
||||
|
||||
from game.game import Game
|
||||
from game.gameState import GameState
|
||||
from game.map import Map
|
||||
|
||||
|
||||
class AiSimulation:
|
||||
"""Own a single Game instance and the callbacks used by that instance."""
|
||||
|
||||
def __init__(self, game_map: Map):
|
||||
self.game = Game(deepcopy(game_map), GameState())
|
||||
self._base_map_types = self.game.map.to_type_dict()
|
||||
min_moves = self.game.map.get_min_moves()
|
||||
self.max_moves = min_moves * 2 if min_moves is not None else None
|
||||
self._condition = Condition()
|
||||
self._pending_move = None
|
||||
self._waiting_for_move = False
|
||||
self._submitted_move = False
|
||||
self._turn_finished = False
|
||||
self._finished = False
|
||||
self._error = None
|
||||
self._thread = None
|
||||
self.moves = []
|
||||
self.game.set_waiting_for_input_callback(self._on_input_request)
|
||||
self.game.set_position_update_callback(self._on_position_update)
|
||||
self.game.set_game_stop_callback(self._should_stop_game)
|
||||
|
||||
def start(self) -> None:
|
||||
# print("Simulation thread starting")
|
||||
self._thread = Thread(target=self._run_game, daemon=True)
|
||||
self._thread.start()
|
||||
with self._condition:
|
||||
self._condition.wait_for(lambda: self._waiting_for_move or self._finished)
|
||||
self._raise_error_if_needed()
|
||||
|
||||
def step(self, move: str | None) -> dict:
|
||||
if move is None:
|
||||
self.stop_for_generation()
|
||||
return self.result()
|
||||
if move not in GameState.PLAYER_MOVEMENTS:
|
||||
raise ValueError(
|
||||
f"Invalid move '{move}'. Must be one of {GameState.PLAYER_MOVEMENTS}."
|
||||
)
|
||||
|
||||
with self._condition:
|
||||
if self._finished:
|
||||
return self.result()
|
||||
self.moves.append(move)
|
||||
self._pending_move = move
|
||||
self._turn_finished = False
|
||||
self._condition.notify_all()
|
||||
self._condition.wait_for(lambda: self._turn_finished or self._finished)
|
||||
self._raise_error_if_needed()
|
||||
return self.result()
|
||||
|
||||
def result(self) -> dict:
|
||||
state = self.game.gameState
|
||||
return {
|
||||
"won": state.gameWon,
|
||||
"coins": state.coins,
|
||||
"moves": state.movesCount,
|
||||
"position": list(state.get_player_position()),
|
||||
"finished": state.stopGame,
|
||||
"changes": self._get_map_changes(),
|
||||
}
|
||||
|
||||
def _get_map_changes(self) -> list[dict]:
|
||||
current_map_types = self.game.map.to_type_dict()
|
||||
changes = []
|
||||
for y, row in enumerate(current_map_types):
|
||||
for x, block_type in enumerate(row):
|
||||
if block_type != self._base_map_types[y][x]:
|
||||
changes.append({"position": [x, y], "type": block_type})
|
||||
return changes
|
||||
|
||||
def close(self) -> None:
|
||||
self.stop_for_generation()
|
||||
|
||||
def stop_for_generation(self) -> None:
|
||||
with self._condition:
|
||||
self._finished = True
|
||||
self.game.stop_game()
|
||||
self._turn_finished = True
|
||||
self._condition.notify_all()
|
||||
|
||||
def _run_game(self) -> None:
|
||||
try:
|
||||
self.game.start_game()
|
||||
except Exception as error:
|
||||
with self._condition:
|
||||
self._error = error
|
||||
self._finished = True
|
||||
self._condition.notify_all()
|
||||
# print(f"Simulation failed: {error}")
|
||||
|
||||
def _on_input_request(self) -> str:
|
||||
with self._condition:
|
||||
if self._submitted_move:
|
||||
self._submitted_move = False
|
||||
self._turn_finished = True
|
||||
self._condition.notify_all()
|
||||
self._waiting_for_move = True
|
||||
self._condition.notify_all()
|
||||
self._condition.wait_for(lambda: self._pending_move is not None or self._finished)
|
||||
if self._finished:
|
||||
return -1
|
||||
move = self._pending_move
|
||||
self._pending_move = None
|
||||
self._waiting_for_move = False
|
||||
self._submitted_move = True
|
||||
return move
|
||||
|
||||
def _on_position_update(self) -> None:
|
||||
with self._condition:
|
||||
if self.game.gameState.stopGame:
|
||||
self._finished = True
|
||||
self._turn_finished = True
|
||||
self._condition.notify_all()
|
||||
|
||||
def _should_stop_game(self, game_state: GameState) -> bool:
|
||||
should_stop = self.max_moves is not None and game_state.movesCount >= self.max_moves
|
||||
if should_stop:
|
||||
with self._condition:
|
||||
self._finished = True
|
||||
self._turn_finished = True
|
||||
self._condition.notify_all()
|
||||
return should_stop
|
||||
|
||||
def _raise_error_if_needed(self) -> None:
|
||||
if self._error:
|
||||
raise self._error
|
||||
@@ -1,12 +1,14 @@
|
||||
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)
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def run(self) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def on_position_update(self) -> None:
|
||||
@@ -15,3 +17,5 @@ class GameHandler:
|
||||
@abstractmethod
|
||||
def on_input_request(self) -> str:
|
||||
pass
|
||||
|
||||
|
||||
@@ -1,24 +1,70 @@
|
||||
|
||||
from time import sleep
|
||||
from game.game import Game
|
||||
from game.handlers.gameHandler import GameHandler
|
||||
|
||||
from game.handlers.gameHandler import GameHandler
|
||||
from game.game import Game
|
||||
from game.gameState import GameState
|
||||
from game.mapReader import MapReader
|
||||
from game.mapSet import MapSet
|
||||
|
||||
class UserPlayerGameHandler(GameHandler):
|
||||
KEYBOARD_INPUTS = ["w", "q", "a", "z", "e", "d", "c"]
|
||||
|
||||
def __init__(self, game: Game):
|
||||
super().__init__(game)
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mapReader = MapReader()
|
||||
self.mapSet = MapSet(self.mapReader, self.mapReader.get_map_names())
|
||||
self.map = self._choose_map()
|
||||
self.gameState = GameState()
|
||||
self.game = Game(self.map, self.gameState)
|
||||
self.game.set_waiting_for_input_callback(self.on_input_request)
|
||||
self.game.set_position_update_callback(self.on_position_update)
|
||||
|
||||
def _choose_map(self):
|
||||
"""
|
||||
Prompt the user to choose a map from the available maps
|
||||
"""
|
||||
print("\nAvailable maps:")
|
||||
for i, map_name in enumerate(self.mapReader.get_map_names()):
|
||||
print(f"[{i + 1}] {map_name.replace("_", " ").title()}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = int(input("Choose a map by number: ")) - 1
|
||||
if 0 <= choice < len(self.mapReader.get_map_names()):
|
||||
return self.mapSet.get_map(choice)
|
||||
else:
|
||||
print("Invalid choice. Please try again.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Run the game loop
|
||||
"""
|
||||
self.game.start_game()
|
||||
print("Game over : You " + ("Won!" if self.game.gameState.gameWon else "Lost!"))
|
||||
|
||||
def on_position_update(self) -> None:
|
||||
"""
|
||||
Refresh the screen
|
||||
"""
|
||||
print("\033[2J\033[H" + self.game.to_string())
|
||||
print("\n\033[2J") # Add margin for stupid vscode terminal
|
||||
|
||||
game_string = self.game.to_string()
|
||||
for i, line in enumerate(game_string.splitlines()):
|
||||
if i == 0:
|
||||
line += "\tCoins: " + str(self.game.gameState.coins)
|
||||
elif i == 1:
|
||||
line += "\tMoves: " + str(self.game.gameState.movesCount)
|
||||
print(line)
|
||||
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)}): "))]
|
||||
user_input = input(f"Enter a movement ({', '.join(self.KEYBOARD_INPUTS)}): ")
|
||||
if user_input not in self.KEYBOARD_INPUTS:
|
||||
print(f"Invalid input: {user_input}. Please enter one of {', '.join(self.KEYBOARD_INPUTS)}.")
|
||||
return self.on_input_request()
|
||||
return self.game.gameState.PLAYER_MOVEMENTS[self.KEYBOARD_INPUTS.index(user_input)]
|
||||
+30
-2
@@ -13,16 +13,21 @@ 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.min_moves = map_data.get('min_moves', None)
|
||||
self.layout = map_data.get('layout', [])
|
||||
self.width = max(len(row) for row in self.layout) if self.layout else 0
|
||||
self.height = len(self.layout) if self.layout else 0
|
||||
|
||||
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 is_position_in_bounds(self, position):
|
||||
x, y = position
|
||||
return 0 <= x < self.width and 0 <= y < self.height
|
||||
|
||||
def get_first_block_type_position(self, block_type):
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
@@ -31,6 +36,13 @@ class Map:
|
||||
return (x, y)
|
||||
return None
|
||||
|
||||
def count_blocks_of_type(self, block_type):
|
||||
return sum(
|
||||
block is not None and block.type == block_type
|
||||
for row in self.layout
|
||||
for block in row
|
||||
)
|
||||
|
||||
def get_block_type_at_position(self, position):
|
||||
x, y = position
|
||||
block = self.get_block_at(x, y)
|
||||
@@ -38,6 +50,9 @@ class Map:
|
||||
return block.type
|
||||
return None
|
||||
|
||||
def get_min_moves(self):
|
||||
return self.min_moves
|
||||
|
||||
def to_string_dict(self):
|
||||
"""
|
||||
Returns a string matrix representation of the map layout.
|
||||
@@ -52,3 +67,16 @@ class Map:
|
||||
string_array_row.append(block.to_string())
|
||||
string_array.append(string_array_row)
|
||||
return string_array
|
||||
|
||||
def to_type_dict(self):
|
||||
"""Returns a matrix containing block types and None for empty spaces."""
|
||||
return [
|
||||
[self._visible_block_type(block) for block in row]
|
||||
for row in self.layout
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _visible_block_type(block):
|
||||
if block is None or getattr(block, "collected", False):
|
||||
return None
|
||||
return block.type
|
||||
+15
-8
@@ -28,14 +28,17 @@ class MapReader:
|
||||
with open(map_path, 'rb') as f:
|
||||
config = tomllib.load(f)
|
||||
|
||||
layout = config.get('layout', '')
|
||||
|
||||
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', ''))
|
||||
'layout': self._parse_layout(layout)
|
||||
}
|
||||
|
||||
if config.get('min_moves') is not None:
|
||||
map_data['min_moves'] = config.get('min_moves')
|
||||
|
||||
return Map(map_data)
|
||||
|
||||
def _get_block_characters_mapping(self):
|
||||
@@ -45,16 +48,20 @@ class MapReader:
|
||||
|
||||
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()
|
||||
# Makes sure tthe width is consistent across all rows by padding with None if necessary.
|
||||
block_mapping = self._get_block_characters_mapping()
|
||||
layout = []
|
||||
for line in mapString.strip().splitlines():
|
||||
for line in mapString.splitlines():
|
||||
row = []
|
||||
for char in line:
|
||||
block_class = block_characters_mapping.get(char)
|
||||
block_class = block_mapping.get(char)
|
||||
if block_class:
|
||||
row.append(block_class())
|
||||
else:
|
||||
row.append(None)
|
||||
row.append(None) # Unknown character, treat as empty space
|
||||
layout.append(row)
|
||||
max_width = max(len(row) for row in layout)
|
||||
for row in layout:
|
||||
while len(row) < max_width:
|
||||
row.append(None) # Pad with None to ensure consistent width
|
||||
return layout
|
||||
|
||||
@@ -10,11 +10,20 @@ class MapSet:
|
||||
self.mapReader = mapReader
|
||||
self.maps = [self.mapReader.read_map(name) for name in mapNames]
|
||||
|
||||
def get_map_by_name(self, name: str):
|
||||
for map in self.maps:
|
||||
if map.name == name:
|
||||
return map
|
||||
raise ValueError(f"Map with name '{name}' not found in the MapSet.")
|
||||
|
||||
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(self):
|
||||
return sorted(self.maps, key=lambda m: m.difficulty)
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from game.game import Game
|
||||
from game.gameState import GameState
|
||||
from game.handlers import GAME_HANDLERS
|
||||
@@ -5,13 +7,15 @@ 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()
|
||||
# loading variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# run_game()
|
||||
if __name__ == "__main__":
|
||||
# Make the user choose the game handler
|
||||
print("Choose a game handler:")
|
||||
for i, handler_name in enumerate(GAME_HANDLERS.keys()):
|
||||
print(f"[{i + 1}] {handler_name}")
|
||||
choice = int(input("Enter the number of the game handler: ")) - 1
|
||||
handler_name = list(GAME_HANDLERS.keys())[choice]
|
||||
game_handler_class = GAME_HANDLERS[handler_name]()
|
||||
game_handler_class.run()
|
||||
@@ -0,0 +1,25 @@
|
||||
difficulty = 90
|
||||
min_moves = 40
|
||||
layout = """
|
||||
#####
|
||||
###### ########
|
||||
## ####
|
||||
### E##
|
||||
### °#####
|
||||
## ° ## ######
|
||||
#°####### ## # # ### ######
|
||||
# # ° ##
|
||||
## ##° ##° ^###
|
||||
# ### #### ##
|
||||
# #####^##### ##
|
||||
# ##### ####° #
|
||||
# # #
|
||||
# °# #####^^##
|
||||
## ° ###^########
|
||||
## # ##########
|
||||
#S ##^#####
|
||||
##########
|
||||
"""
|
||||
|
||||
# Ya moyen d'optimiser le trajet sur les surfaces plates en faisant des sauts au lieu de marcher normalement
|
||||
# Ce serait sympa de voir l'ia optimiser ça
|
||||
@@ -0,0 +1,24 @@
|
||||
difficulty = 85
|
||||
min_moves = 40
|
||||
layout = """
|
||||
#####
|
||||
###### ########
|
||||
## ####
|
||||
### °E##
|
||||
### ° °#####
|
||||
##°°° ° °° ° ° °## ######
|
||||
#°####### ## # # ### ######
|
||||
# #° °°° ##
|
||||
## ##°° ° ##°° ^###
|
||||
# ###°° ####° ##
|
||||
# #####^#####°° ##
|
||||
# #####°####° #
|
||||
# ° °°°°# #
|
||||
# ° °°# #####^^##
|
||||
## ° °###^########
|
||||
## °# ##########
|
||||
#S °##^#####
|
||||
##########
|
||||
"""
|
||||
|
||||
# Big_peanut avec des pièces partout (HAARRRRRR !)
|
||||
+119
-4
@@ -1,7 +1,6 @@
|
||||
width = 12
|
||||
height = 6
|
||||
difficulty = 10
|
||||
layout = """\
|
||||
difficulty = 30
|
||||
min_moves = 7
|
||||
layout = """
|
||||
############
|
||||
# E#
|
||||
# ° °##
|
||||
@@ -9,3 +8,119 @@ layout = """\
|
||||
#S # ##^###
|
||||
############
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#000000000000000000000000000000KKKKKKKKKKKKKKKKKKXXKKKKKXXXXNWMMNNXXXNNNNNNNNWWWWWMMMMMMMMMMMMMMMMMMM
|
||||
#0000000000000000000000000000000KKKKKKKKKKKKKKKKKXWWXKKKKXXXNNWNXXXXXXNNNNNNNMWWWMMMMMMMMMMMMMMMMMMMM
|
||||
#00000000000000000000000000000000KKKKKKKKKKKKKKKKXMMNKKKKXXXXXXXXXXXXXXNNNNNNNWWWWMMMMMMMMMMMMMMMMMM0
|
||||
#OOO0000000000000000000000000000000KKK00KKKKKKKKKKNWXKKKKXXXXXXXXXNNXXXNNNNNNNWWWWMMMMMMMMNX0OOXMMMOl
|
||||
#OOOO00000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXXXNWNXXXNNNNNNWWWWWMMMMMMNkK0xKMMMNdl
|
||||
#OOOO000000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXNWWNXXXXNNNNNNWWWWMMMMMMNXMNKXWWWOol
|
||||
#OOO00000KKKKKKKXXXXKKK000000000000000000KKKKKKKKKKXXXXXKKXXKKXXNWWWWXXXXXNNNNNWWWWWMMMMMMMOkNWWWWdlc
|
||||
#OO00KXNNNNWWWWWWWWWNKK0000000000000000000KKKKKOdcokooONNNNNXXKXNWWWWNXXXXNXNNNWWWWWWMMMMMMOdXWWWXolc
|
||||
#OO0KKKNWNNNNNXXXXKKK00000000000000000000KKKKKd',,coc,'codOXWXXXNWWWWWXXXXXNXNNNWWWWWWMMMMMXx0NNNXool
|
||||
#OOO00000000000000000000000000000000000KKKK0o,',.',,,;;cl:oookKXNWWWNNXXXXXNNNNNWWWWWWMMMMMMxxXNXKlll
|
||||
#OOOOOO000000000000000000000000000000000KKKO:''',,;:ccloddkKOxokXNWWNK0XKXKx0KXNNWWWWWWWMMMM0d0K00ocl
|
||||
#OOOOOOO00000000000000000000000000000000KK0x;'..';cccccoookO00OOXXXKKKKKKK0dkKXNNWNNWWWWWMMMNdkKK0xc:
|
||||
#OOOOOO00000000000000000000000000000000KKKO,...';cllcclldxxkO0OkXK0000K0KKKOo0KXNNNNNWWWWWMMMkkkdl:cc
|
||||
#0O0OOOO00000000000000000000000000000000KK0:...,:lol::::clooodxOXK000K000K0OoO0KXXNNNNWWWWX0x:,:c::::
|
||||
#0OO0OOOO000000000000000000000000000000000O:,;';llc:,''.';lo.;cOXK000000000OdoOO0KXNNNWWWN;;::;::::::
|
||||
#00OOOOOOOOOOO0000000000000000000000000000Oo';,cllccccc:;;:xddk0XK000000000000KKKKlXNNNNNOc::;;::::::
|
||||
#00OOOOOOOOOOOO0000000000000000000000000000x,,;cllcc:cl:',,cdxk0XKKKKK0KK00000000KKKKOx0Oxd:;;;::;;;;
|
||||
#x00OOOOOOOOOOOOOOOO00000000000000000000000kc..colc:c::;,';cdoxKXXXKK000OOOOOOOO000000dOxxk,,,,;;;;;;
|
||||
#xO00OOOOOOOOOOOOOOOOOOO00000000000000000000, cllc:c;'.;odddddkO0K00000OOOkkkkkkxxddddxxcOc',,;;;;;;
|
||||
#Od000OOOOOOOOOOOOOOOOOO000000000000K0KKKXO'. cllccc:;;:oddooookO000OOkxdoolccccloddxkOK0xk',,;;;,,;
|
||||
#0kx000OOOOOOOO0000000000000000000KKKKKKKKx.. .:c:;:;'.,ccclldxxxkO0kl::ccllodxxkOOO000KXXx,,,,,,,,,
|
||||
#00kkOO000000000OOOOO000000KKK000000000000d'... ..'......cclooddddxkOOOkdddxxkkkkOOO0000KXXk,',,,,,,,
|
||||
#00OocokOkkOOOO000KKK000000000Okkxxxxxxdo:.. ....'......:o;oxdllooddxkOOOkxxkkkkkkkOOOO0KXOlc'''',,,,
|
||||
#000Oxk00KK0000000000000000Oxllodddddol:..'. .;;,,,,'..,:cclxxkkocllodxkkOOkxxxxxxkkkOO00dlOl........
|
||||
#00000000000000000000OOOOOkollloddxxxxd:;;x,',:ccc:::;,,:lolcddxxxdolooxxkOOkkxxxxxxkkOkcclc,:;,,....
|
||||
#000000000000000OOkxxddlccllloooodxxddc....:ooccllllol:,,lddo:loddxxxoloooxO0Okxdddoool:c:c:clldo,,,,
|
||||
#000000OOkxxdolccccllllolllloooddddocc....,dkxl:lxxxxxoc,;lool;:oxkkkxoloodxOK0Oxol:,',:ccllllllllloo
|
||||
#kxdoolcclloodddddxxxxxxoooooddddddc,ccxl:dxxxx::kkkkkko;':lloooccooxkxooooxkOKK0dcc;.:....lllllllloo
|
||||
#loddxxxxkkkxxxxxxxxxxxxloooooddxxo:..'c,lxxxddlldk00Okxc,,codxxlo:l::':cooodkO0KKOo:.,....:lllllllll
|
||||
#kkkkkkkkkkkkkkkkxxxxxxxollolllodoc' .cxoc::locck000Oxc;':dxkxcO:d;;,. ;loodkO0XNN0l.'',,:cccccllll
|
||||
#kkkkkkkkkkkkkxxxxxxxxxxol;dl',:l;.. .,,;...',;dl:dkOOxl;'codd:,..',,;;' ;loodk0KNWWN0c'cccccccccccl
|
||||
#kkkkkkkkkkkkkxxxxxxxxxl:,,;cllooc, ,d...;:;;....,;..;ll:;,,:'........':, :lood0XXNWWWNklccc::cccccc
|
||||
#kkkkkkkkkkkkkxxxxddddd:cclloooddoc'.: .:ldd:'....,,,;;.,;';:;',,;,'.... .coood0NNXNNNXxccc:::::ccc
|
||||
#kkkkkkkkkkkkxxxxddddooclllloodddoc. ..'.,'..',,;clolcc:cooc;,.....'. 'looldOXXXXX0xlc:::::::::
|
||||
#kkkkkkkkkkxxxxdddoooooclllooddddol' .......',:loddddolc;cdddooc;'.. .;. ,clllldxkOOOko::::::::::
|
||||
#kkkkkkkxxxxxddddoooollcllooodxxdol' .';:::clloooddxxddlc:lxxkxdoc;,. ,. .,ccccccllodxx:::::;;;;;
|
||||
#kkkkkkxxxxxdddoooolllllllooodxxxdo; .';:ccccllooddddxxxxoc:lxkxxxxlc:'. . .,;:;:::coo:;;;;;;:cll
|
||||
#kkkkxxxxxdddooolllllllclllooodxxxkxdl,. ';:clllllloodxxxxxxdll;:xkkkxdolc,. .'.',,,,;...:llllc;,,
|
||||
#kkkxxxxddddooollllllllcclllloodddxkkkkkoc;cllllcllodxxxkkOkxdxd:dO000OOxxdo:. .,,,...';:clc;;,,,,,,
|
||||
#kkxxxxdddoooolllllllllc:cclllloooddxxxkxxoccllllloodddxxxkxdoc:cdlloodxxkO00k, ..:cccc:;;,,,,,,,,,,,
|
||||
#kxxxxddooooollllclccccc:,,;:ccllloooooccoddoc;;cllcc:;,,oc''',,lkkd:'..'l,.:ll:cc;,,,,,,,,,,,,,,,,,,
|
||||
#xxxxxddooollllccccccccccc:;.,;;::ccllc:ccccc:lxkcclc, ...;lddxO0Oddd:;;:::;,,,,,,,',,',,,',,,,,,,,
|
||||
#xxxdddooollllccccccccccc::;;...,;:::;.;,,'';c;,ldkkkxxdolodxdxkxxdddoc:,,,,,''''''''''''''''''',,,,,
|
||||
#xxdddoolllllccccccc::::::;;. .;;,,. ..;',clooddddxxddxdolc:,,'''''''''''''''''''''''''''''''''
|
||||
#dddooolllllccccc:::::::;;,. .;:,. ..'.... .;:cllooooddo:,'''''''''''''''''''''''''''''''''''''''
|
||||
#dddoolllllcccccc:::::;;;' .;;..,:looodddol:'..';::cccc'....'''.'''....'....''....''''''...''''''
|
||||
#dooollllccccc::::::;;;;. .. ';,':oodddxxxxxxkkxo;. ................................ ...''''''
|
||||
#ooolllccc:::::::;;;;;;. ';;codddxxxxxxxxkkkkkxl. .................................... ...''''''
|
||||
#ollccccc:::::::;;;;;;, ;cloddxxxxkkxkkkkkkkxoc;;::;;;,.........................................
|
||||
#lllcccc:::::;;;;;;;;;' .coodddxxxxkkkkkkkkxxd';::::::::.........................................
|
||||
#lcccc::;;;;;;;;;;;,,,' clooddxxxxxkkkkkkkkxdd:.;:::::::;........................................
|
||||
#cccc::;;;;;;;;,,,,,,,,. :llodddxxxxkkOOOOkkxddoc. ,::cc:;,...................................
|
||||
#c::::;;;;;;,,,,,,,,'''' ;lloooddxxxxkOOOOOkkxdooc. .:c;.. ............................
|
||||
#:::;;,,,,,,,,,,'''''''. .cloooddxxxxkkOOOOkxxdool; .:cc' ....................... ...
|
||||
#::;;,,,,,,,,,,'''''.. ;ooooddxxxkkOOOOkxxddoolc' . .;cc;................. ......
|
||||
#:;;,',,''''''''''.. ,ooooddxxxxkkOkxxddoolllc:. . ,clc........... . ......,:
|
||||
#;;,'''''''''''''. :oooooooddxxkkkxddoollccc:'. . 'cc, ......;:cl
|
||||
#;,'''''''''''.. 'lolllllooooddxxddoollcccc:,. .. .cc . .. .,;cccl
|
||||
#;'.'''...''.. 'coollllllooooooooollcc:::::,. . .c. . . ..';:ccll
|
||||
#.''........ ,loolllllllooooooollcc::::::;,. . ..';::cccl
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
difficulty = 30
|
||||
min_moves = 28
|
||||
layout = """
|
||||
# ° ° ° ° ° ° ° ° ° ° ° #
|
||||
#E #
|
||||
##########################°^#
|
||||
# #°#°# #°#°# #°#°#°###
|
||||
#S ####
|
||||
#############################
|
||||
"""
|
||||
|
||||
# Tutorial to teach that distance is not the optimal way of maximize the score
|
||||
@@ -0,0 +1,23 @@
|
||||
difficulty = 25
|
||||
min_moves = 12
|
||||
layout = """
|
||||
########
|
||||
#S ###
|
||||
### ###
|
||||
# ##
|
||||
# ##
|
||||
# ###
|
||||
# ##
|
||||
# °## ##
|
||||
# ### ##
|
||||
# ° #
|
||||
# ^ #
|
||||
#^## #° #
|
||||
# ##### #
|
||||
# ###### #
|
||||
# ####### #
|
||||
#^ ########^E#
|
||||
###############
|
||||
"""
|
||||
|
||||
# Sort of a jumping tutorial
|
||||
@@ -0,0 +1,27 @@
|
||||
difficulty = 20
|
||||
min_moves = 24
|
||||
layout = """
|
||||
E##
|
||||
####
|
||||
### ########
|
||||
### ### #^#
|
||||
### ## ####
|
||||
### ### #
|
||||
## ####
|
||||
### ##
|
||||
S ### ###
|
||||
#############################
|
||||
# #
|
||||
# ###### ###### #
|
||||
# # # # # #
|
||||
# # # # # #
|
||||
# ###### ###### #
|
||||
# ######### #
|
||||
# # # #
|
||||
# # # # #
|
||||
# # # # # #
|
||||
# # # # # #
|
||||
"""
|
||||
|
||||
# Map made by my mum when asking her to do a "hard map"
|
||||
# I think the hard part is the housing crisis
|
||||
@@ -0,0 +1,21 @@
|
||||
difficulty = 10
|
||||
min_moves = 14
|
||||
layout = """
|
||||
|
||||
# E#
|
||||
#°####
|
||||
## #
|
||||
### #
|
||||
####°#
|
||||
# ##
|
||||
# ###
|
||||
#°####
|
||||
## #
|
||||
### #
|
||||
####°#
|
||||
# ##
|
||||
#S ###
|
||||
######
|
||||
"""
|
||||
|
||||
# Sort of a jumping tutorial
|
||||
@@ -0,0 +1,19 @@
|
||||
difficulty = 70
|
||||
min_moves = 6
|
||||
layout = """
|
||||
# #
|
||||
# S #
|
||||
# ##### #
|
||||
# #
|
||||
# # #
|
||||
# ° #
|
||||
## ° # ##
|
||||
# #^^ #
|
||||
## ° ° ##
|
||||
### # E # # ###
|
||||
###^^^^^^###^^#^^^###
|
||||
#################
|
||||
"""
|
||||
|
||||
# Hard because you have two different controlled descent (going left_down-right_down or left-right)
|
||||
# But you could maybe optimize by collecting at least 3 coins
|
||||
@@ -0,0 +1,9 @@
|
||||
difficulty = 5
|
||||
min_moves = 11
|
||||
layout = """
|
||||
# #
|
||||
#S E#
|
||||
#########################
|
||||
"""
|
||||
|
||||
# You can go 2 blocks by jumping in diagonal instead of simply going left or right
|
||||
@@ -0,0 +1,9 @@
|
||||
difficulty = 7
|
||||
min_moves = 11
|
||||
layout = """
|
||||
# #
|
||||
#E S#
|
||||
#########################
|
||||
"""
|
||||
|
||||
# Teach to not only go right
|
||||
@@ -0,0 +1,25 @@
|
||||
difficulty = 40
|
||||
min_moves = 8
|
||||
layout = """
|
||||
#^ ^#
|
||||
#^ S^#
|
||||
#^ #^#
|
||||
#^ ^#
|
||||
#^# ^#
|
||||
#^ °^#
|
||||
#^ #^#
|
||||
#^# ^#
|
||||
#^ #^#
|
||||
#^# ^#
|
||||
#^ ^#
|
||||
#^ °^#
|
||||
#^ #^#
|
||||
#^ ^#
|
||||
#^ ^#
|
||||
#^ E^#
|
||||
#^##^#
|
||||
#^##^#
|
||||
######
|
||||
"""
|
||||
|
||||
# Needs to have learned to use left_down-right_down
|
||||
@@ -0,0 +1,128 @@
|
||||
difficulty = 70
|
||||
min_moves = 16
|
||||
layout = """
|
||||
|
||||
#####################
|
||||
# ° ° #
|
||||
# ## ## #
|
||||
# °### °### #
|
||||
# #### #### #
|
||||
#S ##### ##### E#
|
||||
#####################
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#000000000000000000000000000000KKKKKKKKKKKKKKKKKKXXKKKKKXXXXNWMMNNXXXNNNNNNNNWWWWWMMMMMMMMMMMMMMMMMMM
|
||||
#0000000000000000000000000000000KKKKKKKKKKKKKKKKKXWWXKKKKXXXNNWNXXXXXXNNNNNNNMWWWMMMMMMMMMMMMMMMMMMMM
|
||||
#00000000000000000000000000000000KKKKKKKKKKKKKKKKXMMNKKKKXXXXXXXXXXXXXXNNNNNNNWWWWMMMMMMMMMMMMMMMMMM0
|
||||
#OOO0000000000000000000000000000000KKK00KKKKKKKKKKNWXKKKKXXXXXXXXXNNXXXNNNNNNNWWWWMMMMMMMMNX0OOXMMMOl
|
||||
#OOOO00000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXXXNWNXXXNNNNNNWWWWWMMMMMMNkK0xKMMMNdl
|
||||
#OOOO000000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXNWWNXXXXNNNNNNWWWWMMMMMMNXMNKXWWWOol
|
||||
#OOO00000KKKKKKKXXXXKKK000000000000000000KKKKKKKKKKXXXXXKKXXKKXXNWWWWXXXXXNNNNNWWWWWMMMMMMMOkNWWWWdlc
|
||||
#OO00KXNNNNWWWWWWWWWNKK0000000000000000000KKKKKOdcokooONNNNNXXKXNWWWWNXXXXNXNNNWWWWWWMMMMMMOdXWWWXolc
|
||||
#OO0KKKNWNNNNNXXXXKKK00000000000000000000KKKKKd',,coc,'codOXWXXXNWWWWWXXXXXNXNNNWWWWWWMMMMMXx0NNNXool
|
||||
#OOO00000000000000000000000000000000000KKKK0o,',.',,,;;cl:oookKXNWWWNNXXXXXNNNNNWWWWWWMMMMMMxxXNXKlll
|
||||
#OOOOOO000000000000000000000000000000000KKKO:''',,;:ccloddkKOxokXNWWNK0XKXKx0KXNNWWWWWWWMMMM0d0K00ocl
|
||||
#OOOOOOO00000000000000000000000000000000KK0x;'..';cccccoookO00OOXXXKKKKKKK0dkKXNNWNNWWWWWMMMNdkKK0xc:
|
||||
#OOOOOO00000000000000000000000000000000KKKO,...';cllcclldxxkO0OkXK0000K0KKKOo0KXNNNNNWWWWWMMMkkkdl:cc
|
||||
#0O0OOOO00000000000000000000000000000000KK0:...,:lol::::clooodxOXK000K000K0OoO0KXXNNNNWWWWX0x:,:c::::
|
||||
#0OO0OOOO000000000000000000000000000000000O:,;';llc:,''.';lo.;cOXK000000000OdoOO0KXNNNWWWN;;::;::::::
|
||||
#00OOOOOOOOOOO0000000000000000000000000000Oo';,cllccccc:;;:xddk0XK000000000000KKKKlXNNNNNOc::;;::::::
|
||||
#00OOOOOOOOOOOO0000000000000000000000000000x,,;cllcc:cl:',,cdxk0XKKKKK0KK00000000KKKKOx0Oxd:;;;::;;;;
|
||||
#x00OOOOOOOOOOOOOOOO00000000000000000000000kc..colc:c::;,';cdoxKXXXKK000OOOOOOOO000000dOxxk,,,,;;;;;;
|
||||
#xO00OOOOOOOOOOOOOOOOOOO00000000000000000000, cllc:c;'.;odddddkO0K00000OOOkkkkkkxxddddxxcOc',,;;;;;;
|
||||
#Od000OOOOOOOOOOOOOOOOOO000000000000K0KKKXO'. cllccc:;;:oddooookO000OOkxdoolccccloddxkOK0xk',,;;;,,;
|
||||
#0kx000OOOOOOOO0000000000000000000KKKKKKKKx.. .:c:;:;'.,ccclldxxxkO0kl::ccllodxxkOOO000KXXx,,,,,,,,,
|
||||
#00kkOO000000000OOOOO000000KKK000000000000d'... ..'......cclooddddxkOOOkdddxxkkkkOOO0000KXXk,',,,,,,,
|
||||
#00OocokOkkOOOO000KKK000000000Okkxxxxxxdo:.. ....'......:o;oxdllooddxkOOOkxxkkkkkkkOOOO0KXOlc'''',,,,
|
||||
#000Oxk00KK0000000000000000Oxllodddddol:..'. .;;,,,,'..,:cclxxkkocllodxkkOOkxxxxxxkkkOO00dlOl........
|
||||
#00000000000000000000OOOOOkollloddxxxxd:;;x,',:ccc:::;,,:lolcddxxxdolooxxkOOkkxxxxxxkkOkcclc,:;,,....
|
||||
#000000000000000OOkxxddlccllloooodxxddc....:ooccllllol:,,lddo:loddxxxoloooxO0Okxdddoool:c:c:clldo,,,,
|
||||
#000000OOkxxdolccccllllolllloooddddocc....,dkxl:lxxxxxoc,;lool;:oxkkkxoloodxOK0Oxol:,',:ccllllllllloo
|
||||
#kxdoolcclloodddddxxxxxxoooooddddddc,ccxl:dxxxx::kkkkkko;':lloooccooxkxooooxkOKK0dcc;.:....lllllllloo
|
||||
#loddxxxxkkkxxxxxxxxxxxxloooooddxxo:..'c,lxxxddlldk00Okxc,,codxxlo:l::':cooodkO0KKOo:.,....:lllllllll
|
||||
#kkkkkkkkkkkkkkkkxxxxxxxollolllodoc' .cxoc::locck000Oxc;':dxkxcO:d;;,. ;loodkO0XNN0l.'',,:cccccllll
|
||||
#kkkkkkkkkkkkkxxxxxxxxxxol;dl',:l;.. .,,;...',;dl:dkOOxl;'codd:,..',,;;' ;loodk0KNWWN0c'cccccccccccl
|
||||
#kkkkkkkkkkkkkxxxxxxxxxl:,,;cllooc, ,d...;:;;....,;..;ll:;,,:'........':, :lood0XXNWWWNklccc::cccccc
|
||||
#kkkkkkkkkkkkkxxxxddddd:cclloooddoc'.: .:ldd:'....,,,;;.,;';:;',,;,'.... .coood0NNXNNNXxccc:::::ccc
|
||||
#kkkkkkkkkkkkxxxxddddooclllloodddoc. ..'.,'..',,;clolcc:cooc;,.....'. 'looldOXXXXX0xlc:::::::::
|
||||
#kkkkkkkkkkxxxxdddoooooclllooddddol' .......',:loddddolc;cdddooc;'.. .;. ,clllldxkOOOko::::::::::
|
||||
#kkkkkkkxxxxxddddoooollcllooodxxdol' .';:::clloooddxxddlc:lxxkxdoc;,. ,. .,ccccccllodxx:::::;;;;;
|
||||
#kkkkkkxxxxxdddoooolllllllooodxxxdo; .';:ccccllooddddxxxxoc:lxkxxxxlc:'. . .,;:;:::coo:;;;;;;:cll
|
||||
#kkkkxxxxxdddooolllllllclllooodxxxkxdl,. ';:clllllloodxxxxxxdll;:xkkkxdolc,. .'.',,,,;...:llllc;,,
|
||||
#kkkxxxxddddooollllllllcclllloodddxkkkkkoc;cllllcllodxxxkkOkxdxd:dO000OOxxdo:. .,,,...';:clc;;,,,,,,
|
||||
#kkxxxxdddoooolllllllllc:cclllloooddxxxkxxoccllllloodddxxxkxdoc:cdlloodxxkO00k, ..:cccc:;;,,,,,,,,,,,
|
||||
#kxxxxddooooollllclccccc:,,;:ccllloooooccoddoc;;cllcc:;,,oc''',,lkkd:'..'l,.:ll:cc;,,,,,,,,,,,,,,,,,,
|
||||
#xxxxxddooollllccccccccccc:;.,;;::ccllc:ccccc:lxkcclc, ...;lddxO0Oddd:;;:::;,,,,,,,',,',,,',,,,,,,,
|
||||
#xxxdddooollllccccccccccc::;;...,;:::;.;,,'';c;,ldkkkxxdolodxdxkxxdddoc:,,,,,''''''''''''''''''',,,,,
|
||||
#xxdddoolllllccccccc::::::;;. .;;,,. ..;',clooddddxxddxdolc:,,'''''''''''''''''''''''''''''''''
|
||||
#dddooolllllccccc:::::::;;,. .;:,. ..'.... .;:cllooooddo:,'''''''''''''''''''''''''''''''''''''''
|
||||
#dddoolllllcccccc:::::;;;' .;;..,:looodddol:'..';::cccc'....'''.'''....'....''....''''''...''''''
|
||||
#dooollllccccc::::::;;;;. .. ';,':oodddxxxxxxkkxo;. ................................ ...''''''
|
||||
#ooolllccc:::::::;;;;;;. ';;codddxxxxxxxxkkkkkxl. .................................... ...''''''
|
||||
#ollccccc:::::::;;;;;;, ;cloddxxxxkkxkkkkkkkxoc;;::;;;,.........................................
|
||||
#lllcccc:::::;;;;;;;;;' .coodddxxxxkkkkkkkkxxd';::::::::.........................................
|
||||
#lcccc::;;;;;;;;;;;,,,' clooddxxxxxkkkkkkkkxdd:.;:::::::;........................................
|
||||
#cccc::;;;;;;;;,,,,,,,,. :llodddxxxxkkOOOOkkxddoc. ,::cc:;,...................................
|
||||
#c::::;;;;;;,,,,,,,,'''' ;lloooddxxxxkOOOOOkkxdooc. .:c;.. ............................
|
||||
#:::;;,,,,,,,,,,'''''''. .cloooddxxxxkkOOOOkxxdool; .:cc' ....................... ...
|
||||
#::;;,,,,,,,,,,'''''.. ;ooooddxxxkkOOOOkxxddoolc' . .;cc;................. ......
|
||||
#:;;,',,''''''''''.. ,ooooddxxxxkkOkxxddoolllc:. . ,clc........... . ......,:
|
||||
#;;,'''''''''''''. :oooooooddxxkkkxddoollccc:'. . 'cc, ......;:cl
|
||||
#;,'''''''''''.. 'lolllllooooddxxddoollcccc:,. .. .cc . .. .,;cccl
|
||||
#;'.'''...''.. 'coollllllooooooooollcc:::::,. . .c. . . ..';:ccll
|
||||
#.''........ ,loolllllllooooooollcc::::::;,. . ..';::cccl
|
||||
@@ -0,0 +1,131 @@
|
||||
difficulty = 80
|
||||
min_moves = 28
|
||||
layout = """
|
||||
####################
|
||||
########## E#####
|
||||
######### ##########
|
||||
########## ########
|
||||
########### #
|
||||
############ #### #
|
||||
# ## ###
|
||||
# #### ^^^ ####
|
||||
# ##### ### #####
|
||||
#S###### ######
|
||||
####################
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#000000000000000000000000000000KKKKKKKKKKKKKKKKKKXXKKKKKXXXXNWMMNNXXXNNNNNNNNWWWWWMMMMMMMMMMMMMMMMMMM
|
||||
#0000000000000000000000000000000KKKKKKKKKKKKKKKKKXWWXKKKKXXXNNWNXXXXXXNNNNNNNMWWWMMMMMMMMMMMMMMMMMMMM
|
||||
#00000000000000000000000000000000KKKKKKKKKKKKKKKKXMMNKKKKXXXXXXXXXXXXXXNNNNNNNWWWWMMMMMMMMMMMMMMMMMM0
|
||||
#OOO0000000000000000000000000000000KKK00KKKKKKKKKKNWXKKKKXXXXXXXXXNNXXXNNNNNNNWWWWMMMMMMMMNX0OOXMMMOl
|
||||
#OOOO00000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXXXNWNXXXNNNNNNWWWWWMMMMMMNkK0xKMMMNdl
|
||||
#OOOO000000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXNWWNXXXXNNNNNNWWWWMMMMMMNXMNKXWWWOol
|
||||
#OOO00000KKKKKKKXXXXKKK000000000000000000KKKKKKKKKKXXXXXKKXXKKXXNWWWWXXXXXNNNNNWWWWWMMMMMMMOkNWWWWdlc
|
||||
#OO00KXNNNNWWWWWWWWWNKK0000000000000000000KKKKKOdcokooONNNNNXXKXNWWWWNXXXXNXNNNWWWWWWMMMMMMOdXWWWXolc
|
||||
#OO0KKKNWNNNNNXXXXKKK00000000000000000000KKKKKd',,coc,'codOXWXXXNWWWWWXXXXXNXNNNWWWWWWMMMMMXx0NNNXool
|
||||
#OOO00000000000000000000000000000000000KKKK0o,',.',,,;;cl:oookKXNWWWNNXXXXXNNNNNWWWWWWMMMMMMxxXNXKlll
|
||||
#OOOOOO000000000000000000000000000000000KKKO:''',,;:ccloddkKOxokXNWWNK0XKXKx0KXNNWWWWWWWMMMM0d0K00ocl
|
||||
#OOOOOOO00000000000000000000000000000000KK0x;'..';cccccoookO00OOXXXKKKKKKK0dkKXNNWNNWWWWWMMMNdkKK0xc:
|
||||
#OOOOOO00000000000000000000000000000000KKKO,...';cllcclldxxkO0OkXK0000K0KKKOo0KXNNNNNWWWWWMMMkkkdl:cc
|
||||
#0O0OOOO00000000000000000000000000000000KK0:...,:lol::::clooodxOXK000K000K0OoO0KXXNNNNWWWWX0x:,:c::::
|
||||
#0OO0OOOO000000000000000000000000000000000O:,;';llc:,''.';lo.;cOXK000000000OdoOO0KXNNNWWWN;;::;::::::
|
||||
#00OOOOOOOOOOO0000000000000000000000000000Oo';,cllccccc:;;:xddk0XK000000000000KKKKlXNNNNNOc::;;::::::
|
||||
#00OOOOOOOOOOOO0000000000000000000000000000x,,;cllcc:cl:',,cdxk0XKKKKK0KK00000000KKKKOx0Oxd:;;;::;;;;
|
||||
#x00OOOOOOOOOOOOOOOO00000000000000000000000kc..colc:c::;,';cdoxKXXXKK000OOOOOOOO000000dOxxk,,,,;;;;;;
|
||||
#xO00OOOOOOOOOOOOOOOOOOO00000000000000000000, cllc:c;'.;odddddkO0K00000OOOkkkkkkxxddddxxcOc',,;;;;;;
|
||||
#Od000OOOOOOOOOOOOOOOOOO000000000000K0KKKXO'. cllccc:;;:oddooookO000OOkxdoolccccloddxkOK0xk',,;;;,,;
|
||||
#0kx000OOOOOOOO0000000000000000000KKKKKKKKx.. .:c:;:;'.,ccclldxxxkO0kl::ccllodxxkOOO000KXXx,,,,,,,,,
|
||||
#00kkOO000000000OOOOO000000KKK000000000000d'... ..'......cclooddddxkOOOkdddxxkkkkOOO0000KXXk,',,,,,,,
|
||||
#00OocokOkkOOOO000KKK000000000Okkxxxxxxdo:.. ....'......:o;oxdllooddxkOOOkxxkkkkkkkOOOO0KXOlc'''',,,,
|
||||
#000Oxk00KK0000000000000000Oxllodddddol:..'. .;;,,,,'..,:cclxxkkocllodxkkOOkxxxxxxkkkOO00dlOl........
|
||||
#00000000000000000000OOOOOkollloddxxxxd:;;x,',:ccc:::;,,:lolcddxxxdolooxxkOOkkxxxxxxkkOkcclc,:;,,....
|
||||
#000000000000000OOkxxddlccllloooodxxddc....:ooccllllol:,,lddo:loddxxxoloooxO0Okxdddoool:c:c:clldo,,,,
|
||||
#000000OOkxxdolccccllllolllloooddddocc....,dkxl:lxxxxxoc,;lool;:oxkkkxoloodxOK0Oxol:,',:ccllllllllloo
|
||||
#kxdoolcclloodddddxxxxxxoooooddddddc,ccxl:dxxxx::kkkkkko;':lloooccooxkxooooxkOKK0dcc;.:....lllllllloo
|
||||
#loddxxxxkkkxxxxxxxxxxxxloooooddxxo:..'c,lxxxddlldk00Okxc,,codxxlo:l::':cooodkO0KKOo:.,....:lllllllll
|
||||
#kkkkkkkkkkkkkkkkxxxxxxxollolllodoc' .cxoc::locck000Oxc;':dxkxcO:d;;,. ;loodkO0XNN0l.'',,:cccccllll
|
||||
#kkkkkkkkkkkkkxxxxxxxxxxol;dl',:l;.. .,,;...',;dl:dkOOxl;'codd:,..',,;;' ;loodk0KNWWN0c'cccccccccccl
|
||||
#kkkkkkkkkkkkkxxxxxxxxxl:,,;cllooc, ,d...;:;;....,;..;ll:;,,:'........':, :lood0XXNWWWNklccc::cccccc
|
||||
#kkkkkkkkkkkkkxxxxddddd:cclloooddoc'.: .:ldd:'....,,,;;.,;';:;',,;,'.... .coood0NNXNNNXxccc:::::ccc
|
||||
#kkkkkkkkkkkkxxxxddddooclllloodddoc. ..'.,'..',,;clolcc:cooc;,.....'. 'looldOXXXXX0xlc:::::::::
|
||||
#kkkkkkkkkkxxxxdddoooooclllooddddol' .......',:loddddolc;cdddooc;'.. .;. ,clllldxkOOOko::::::::::
|
||||
#kkkkkkkxxxxxddddoooollcllooodxxdol' .';:::clloooddxxddlc:lxxkxdoc;,. ,. .,ccccccllodxx:::::;;;;;
|
||||
#kkkkkkxxxxxdddoooolllllllooodxxxdo; .';:ccccllooddddxxxxoc:lxkxxxxlc:'. . .,;:;:::coo:;;;;;;:cll
|
||||
#kkkkxxxxxdddooolllllllclllooodxxxkxdl,. ';:clllllloodxxxxxxdll;:xkkkxdolc,. .'.',,,,;...:llllc;,,
|
||||
#kkkxxxxddddooollllllllcclllloodddxkkkkkoc;cllllcllodxxxkkOkxdxd:dO000OOxxdo:. .,,,...';:clc;;,,,,,,
|
||||
#kkxxxxdddoooolllllllllc:cclllloooddxxxkxxoccllllloodddxxxkxdoc:cdlloodxxkO00k, ..:cccc:;;,,,,,,,,,,,
|
||||
#kxxxxddooooollllclccccc:,,;:ccllloooooccoddoc;;cllcc:;,,oc''',,lkkd:'..'l,.:ll:cc;,,,,,,,,,,,,,,,,,,
|
||||
#xxxxxddooollllccccccccccc:;.,;;::ccllc:ccccc:lxkcclc, ...;lddxO0Oddd:;;:::;,,,,,,,',,',,,',,,,,,,,
|
||||
#xxxdddooollllccccccccccc::;;...,;:::;.;,,'';c;,ldkkkxxdolodxdxkxxdddoc:,,,,,''''''''''''''''''',,,,,
|
||||
#xxdddoolllllccccccc::::::;;. .;;,,. ..;',clooddddxxddxdolc:,,'''''''''''''''''''''''''''''''''
|
||||
#dddooolllllccccc:::::::;;,. .;:,. ..'.... .;:cllooooddo:,'''''''''''''''''''''''''''''''''''''''
|
||||
#dddoolllllcccccc:::::;;;' .;;..,:looodddol:'..';::cccc'....'''.'''....'....''....''''''...''''''
|
||||
#dooollllccccc::::::;;;;. .. ';,':oodddxxxxxxkkxo;. ................................ ...''''''
|
||||
#ooolllccc:::::::;;;;;;. ';;codddxxxxxxxxkkkkkxl. .................................... ...''''''
|
||||
#ollccccc:::::::;;;;;;, ;cloddxxxxkkxkkkkkkkxoc;;::;;;,.........................................
|
||||
#lllcccc:::::;;;;;;;;;' .coodddxxxxkkkkkkkkxxd';::::::::.........................................
|
||||
#lcccc::;;;;;;;;;;;,,,' clooddxxxxxkkkkkkkkxdd:.;:::::::;........................................
|
||||
#cccc::;;;;;;;;,,,,,,,,. :llodddxxxxkkOOOOkkxddoc. ,::cc:;,...................................
|
||||
#c::::;;;;;;,,,,,,,,'''' ;lloooddxxxxkOOOOOkkxdooc. .:c;.. ............................
|
||||
#:::;;,,,,,,,,,,'''''''. .cloooddxxxxkkOOOOkxxdool; .:cc' ....................... ...
|
||||
#::;;,,,,,,,,,,'''''.. ;ooooddxxxkkOOOOkxxddoolc' . .;cc;................. ......
|
||||
#:;;,',,''''''''''.. ,ooooddxxxxkkOkxxddoolllc:. . ,clc........... . ......,:
|
||||
#;;,'''''''''''''. :oooooooddxxkkkxddoollccc:'. . 'cc, ......;:cl
|
||||
#;,'''''''''''.. 'lolllllooooddxxddoollcccc:,. .. .cc . .. .,;cccl
|
||||
#;'.'''...''.. 'coollllllooooooooollcc:::::,. . .c. . . ..';:ccll
|
||||
#.''........ ,loolllllllooooooollcc::::::;,. . ..';::cccl
|
||||
@@ -0,0 +1,123 @@
|
||||
difficulty = 60
|
||||
min_moves = 20
|
||||
layout = """
|
||||
# # ## ##° ########### #
|
||||
#S ° ° °# ° ° E#
|
||||
###############################
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#000000000000000000000000000000KKKKKKKKKKKKKKKKKKXXKKKKKXXXXNWMMNNXXXNNNNNNNNWWWWWMMMMMMMMMMMMMMMMMMM
|
||||
#0000000000000000000000000000000KKKKKKKKKKKKKKKKKXWWXKKKKXXXNNWNXXXXXXNNNNNNNMWWWMMMMMMMMMMMMMMMMMMMM
|
||||
#00000000000000000000000000000000KKKKKKKKKKKKKKKKXMMNKKKKXXXXXXXXXXXXXXNNNNNNNWWWWMMMMMMMMMMMMMMMMMM0
|
||||
#OOO0000000000000000000000000000000KKK00KKKKKKKKKKNWXKKKKXXXXXXXXXNNXXXNNNNNNNWWWWMMMMMMMMNX0OOXMMMOl
|
||||
#OOOO00000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXXXNWNXXXNNNNNNWWWWWMMMMMMNkK0xKMMMNdl
|
||||
#OOOO000000000000000000000000000000KKK00000KKKKKKKKKKKKKKKKXXXXXXNWWNXXXXNNNNNNWWWWMMMMMMNXMNKXWWWOol
|
||||
#OOO00000KKKKKKKXXXXKKK000000000000000000KKKKKKKKKKXXXXXKKXXKKXXNWWWWXXXXXNNNNNWWWWWMMMMMMMOkNWWWWdlc
|
||||
#OO00KXNNNNWWWWWWWWWNKK0000000000000000000KKKKKOdcokooONNNNNXXKXNWWWWNXXXXNXNNNWWWWWWMMMMMMOdXWWWXolc
|
||||
#OO0KKKNWNNNNNXXXXKKK00000000000000000000KKKKKd',,coc,'codOXWXXXNWWWWWXXXXXNXNNNWWWWWWMMMMMXx0NNNXool
|
||||
#OOO00000000000000000000000000000000000KKKK0o,',.',,,;;cl:oookKXNWWWNNXXXXXNNNNNWWWWWWMMMMMMxxXNXKlll
|
||||
#OOOOOO000000000000000000000000000000000KKKO:''',,;:ccloddkKOxokXNWWNK0XKXKx0KXNNWWWWWWWMMMM0d0K00ocl
|
||||
#OOOOOOO00000000000000000000000000000000KK0x;'..';cccccoookO00OOXXXKKKKKKK0dkKXNNWNNWWWWWMMMNdkKK0xc:
|
||||
#OOOOOO00000000000000000000000000000000KKKO,...';cllcclldxxkO0OkXK0000K0KKKOo0KXNNNNNWWWWWMMMkkkdl:cc
|
||||
#0O0OOOO00000000000000000000000000000000KK0:...,:lol::::clooodxOXK000K000K0OoO0KXXNNNNWWWWX0x:,:c::::
|
||||
#0OO0OOOO000000000000000000000000000000000O:,;';llc:,''.';lo.;cOXK000000000OdoOO0KXNNNWWWN;;::;::::::
|
||||
#00OOOOOOOOOOO0000000000000000000000000000Oo';,cllccccc:;;:xddk0XK000000000000KKKKlXNNNNNOc::;;::::::
|
||||
#00OOOOOOOOOOOO0000000000000000000000000000x,,;cllcc:cl:',,cdxk0XKKKKK0KK00000000KKKKOx0Oxd:;;;::;;;;
|
||||
#x00OOOOOOOOOOOOOOOO00000000000000000000000kc..colc:c::;,';cdoxKXXXKK000OOOOOOOO000000dOxxk,,,,;;;;;;
|
||||
#xO00OOOOOOOOOOOOOOOOOOO00000000000000000000, cllc:c;'.;odddddkO0K00000OOOkkkkkkxxddddxxcOc',,;;;;;;
|
||||
#Od000OOOOOOOOOOOOOOOOOO000000000000K0KKKXO'. cllccc:;;:oddooookO000OOkxdoolccccloddxkOK0xk',,;;;,,;
|
||||
#0kx000OOOOOOOO0000000000000000000KKKKKKKKx.. .:c:;:;'.,ccclldxxxkO0kl::ccllodxxkOOO000KXXx,,,,,,,,,
|
||||
#00kkOO000000000OOOOO000000KKK000000000000d'... ..'......cclooddddxkOOOkdddxxkkkkOOO0000KXXk,',,,,,,,
|
||||
#00OocokOkkOOOO000KKK000000000Okkxxxxxxdo:.. ....'......:o;oxdllooddxkOOOkxxkkkkkkkOOOO0KXOlc'''',,,,
|
||||
#000Oxk00KK0000000000000000Oxllodddddol:..'. .;;,,,,'..,:cclxxkkocllodxkkOOkxxxxxxkkkOO00dlOl........
|
||||
#00000000000000000000OOOOOkollloddxxxxd:;;x,',:ccc:::;,,:lolcddxxxdolooxxkOOkkxxxxxxkkOkcclc,:;,,....
|
||||
#000000000000000OOkxxddlccllloooodxxddc....:ooccllllol:,,lddo:loddxxxoloooxO0Okxdddoool:c:c:clldo,,,,
|
||||
#000000OOkxxdolccccllllolllloooddddocc....,dkxl:lxxxxxoc,;lool;:oxkkkxoloodxOK0Oxol:,',:ccllllllllloo
|
||||
#kxdoolcclloodddddxxxxxxoooooddddddc,ccxl:dxxxx::kkkkkko;':lloooccooxkxooooxkOKK0dcc;.:....lllllllloo
|
||||
#loddxxxxkkkxxxxxxxxxxxxloooooddxxo:..'c,lxxxddlldk00Okxc,,codxxlo:l::':cooodkO0KKOo:.,....:lllllllll
|
||||
#kkkkkkkkkkkkkkkkxxxxxxxollolllodoc' .cxoc::locck000Oxc;':dxkxcO:d;;,. ;loodkO0XNN0l.'',,:cccccllll
|
||||
#kkkkkkkkkkkkkxxxxxxxxxxol;dl',:l;.. .,,;...',;dl:dkOOxl;'codd:,..',,;;' ;loodk0KNWWN0c'cccccccccccl
|
||||
#kkkkkkkkkkkkkxxxxxxxxxl:,,;cllooc, ,d...;:;;....,;..;ll:;,,:'........':, :lood0XXNWWWNklccc::cccccc
|
||||
#kkkkkkkkkkkkkxxxxddddd:cclloooddoc'.: .:ldd:'....,,,;;.,;';:;',,;,'.... .coood0NNXNNNXxccc:::::ccc
|
||||
#kkkkkkkkkkkkxxxxddddooclllloodddoc. ..'.,'..',,;clolcc:cooc;,.....'. 'looldOXXXXX0xlc:::::::::
|
||||
#kkkkkkkkkkxxxxdddoooooclllooddddol' .......',:loddddolc;cdddooc;'.. .;. ,clllldxkOOOko::::::::::
|
||||
#kkkkkkkxxxxxddddoooollcllooodxxdol' .';:::clloooddxxddlc:lxxkxdoc;,. ,. .,ccccccllodxx:::::;;;;;
|
||||
#kkkkkkxxxxxdddoooolllllllooodxxxdo; .';:ccccllooddddxxxxoc:lxkxxxxlc:'. . .,;:;:::coo:;;;;;;:cll
|
||||
#kkkkxxxxxdddooolllllllclllooodxxxkxdl,. ';:clllllloodxxxxxxdll;:xkkkxdolc,. .'.',,,,;...:llllc;,,
|
||||
#kkkxxxxddddooollllllllcclllloodddxkkkkkoc;cllllcllodxxxkkOkxdxd:dO000OOxxdo:. .,,,...';:clc;;,,,,,,
|
||||
#kkxxxxdddoooolllllllllc:cclllloooddxxxkxxoccllllloodddxxxkxdoc:cdlloodxxkO00k, ..:cccc:;;,,,,,,,,,,,
|
||||
#kxxxxddooooollllclccccc:,,;:ccllloooooccoddoc;;cllcc:;,,oc''',,lkkd:'..'l,.:ll:cc;,,,,,,,,,,,,,,,,,,
|
||||
#xxxxxddooollllccccccccccc:;.,;;::ccllc:ccccc:lxkcclc, ...;lddxO0Oddd:;;:::;,,,,,,,',,',,,',,,,,,,,
|
||||
#xxxdddooollllccccccccccc::;;...,;:::;.;,,'';c;,ldkkkxxdolodxdxkxxdddoc:,,,,,''''''''''''''''''',,,,,
|
||||
#xxdddoolllllccccccc::::::;;. .;;,,. ..;',clooddddxxddxdolc:,,'''''''''''''''''''''''''''''''''
|
||||
#dddooolllllccccc:::::::;;,. .;:,. ..'.... .;:cllooooddo:,'''''''''''''''''''''''''''''''''''''''
|
||||
#dddoolllllcccccc:::::;;;' .;;..,:looodddol:'..';::cccc'....'''.'''....'....''....''''''...''''''
|
||||
#dooollllccccc::::::;;;;. .. ';,':oodddxxxxxxkkxo;. ................................ ...''''''
|
||||
#ooolllccc:::::::;;;;;;. ';;codddxxxxxxxxkkkkkxl. .................................... ...''''''
|
||||
#ollccccc:::::::;;;;;;, ;cloddxxxxkkxkkkkkkkxoc;;::;;;,.........................................
|
||||
#lllcccc:::::;;;;;;;;;' .coodddxxxxkkkkkkkkxxd';::::::::.........................................
|
||||
#lcccc::;;;;;;;;;;;,,,' clooddxxxxxkkkkkkkkxdd:.;:::::::;........................................
|
||||
#cccc::;;;;;;;;,,,,,,,,. :llodddxxxxkkOOOOkkxddoc. ,::cc:;,...................................
|
||||
#c::::;;;;;;,,,,,,,,'''' ;lloooddxxxxkOOOOOkkxdooc. .:c;.. ............................
|
||||
#:::;;,,,,,,,,,,'''''''. .cloooddxxxxkkOOOOkxxdool; .:cc' ....................... ...
|
||||
#::;;,,,,,,,,,,'''''.. ;ooooddxxxkkOOOOkxxddoolc' . .;cc;................. ......
|
||||
#:;;,',,''''''''''.. ,ooooddxxxxkkOkxxddoolllc:. . ,clc........... . ......,:
|
||||
#;;,'''''''''''''. :oooooooddxxkkkxddoollccc:'. . 'cc, ......;:cl
|
||||
#;,'''''''''''.. 'lolllllooooddxxddoollcccc:,. .. .cc . .. .,;cccl
|
||||
#;'.'''...''.. 'coollllllooooooooollcc:::::,. . .c. . . ..';:ccll
|
||||
#.''........ ,loolllllllooooooollcc::::::;,. . ..';::cccl
|
||||
Reference in New Issue
Block a user