Compare commits

..

21 Commits

Author SHA1 Message Date
Ninluc d6e2fc7be9 Minimum performance delta 2026-11-03 08:30:41 +01:00
Ninluc 3d2f98d56e Added tunnel mix map 2026-11-03 08:30:28 +01:00
Ninluc 0fd490b97c Score weights Rebalancing 2026-10-31 17:00:21 +01:00
Ninluc 104297e88b Added intermediate maps 2026-10-31 17:00:12 +01:00
Ninluc 104c6fac63 Better performance score 2026-10-31 14:50:25 +01:00
Ninluc a7a3d973a3 Play all maps each generation
Also disabled some too hard maps
2026-10-26 15:53:27 +01:00
Ninluc f56b8c1e9c Better scoring and map selection + Map delta 2026-10-23 17:54:13 +02:00
Ninluc ce720a92b2 AI TCP Server 2026-10-17 16:50:11 +02:00
Ninluc a3fdce7df4 Verify player is in bound of the map 2026-10-17 16:49:46 +02:00
Ninluc d7e1711103 Fix flying bug 2026-10-17 16:49:29 +02:00
Ninluc 5a78bb3b9d Added map and added coins to big_peanut 2026-10-17 15:01:28 +02:00
Ninluc e8d04bfdc3 Fix jump tutorial map 2026-10-17 15:01:01 +02:00
Ninluc 84d5bd9e4d New Maps 2026-10-11 10:02:21 +02:00
Ninluc 2489138167 Removed .env 2026-10-11 09:35:14 +02:00
Ninluc 7553c293ff Important message 2026-10-10 16:47:35 +02:00
Ninluc c891b4d6f4 Misc.updates and new straight line map 2026-10-10 16:34:22 +02:00
Ninluc 7d3432538e Big peanut update min moves with route optimisation 2026-10-10 16:23:05 +02:00
Ninluc 17f00ebca2 Spikes, better gameHandler, map width and height computed automatically & big peanut hard map 2026-10-10 16:21:11 +02:00
Ninluc 14a39b7154 Added ap min_moves, gameStop callback and optimizations 2026-10-10 15:29:20 +02:00
Ninluc 10a3d93821 Added coins 2026-10-10 15:28:42 +02:00
Ninluc 9030146ec8 Optimise physic 2026-10-10 14:53:04 +02:00
33 changed files with 1624 additions and 81 deletions
+9
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
.venv
venv
__pycache__/
.env
+118
View File
@@ -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":"..."}`.
+4
View File
@@ -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é
+4
View File
@@ -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,
]
+28
View File
@@ -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 "")
+1 -1
View File
@@ -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 "🏁"
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -1,5 +1,7 @@
from .userPlayerGameHandler import UserPlayerGameHandler
from .aiHttpGameHandler import AiHttpGameHandler
GAME_HANDLERS= {
"user player": UserPlayerGameHandler,
"ai tcp": AiHttpGameHandler,
}
+405
View File
@@ -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.")
+136
View File
@@ -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
+9 -5
View File
@@ -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
+53 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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.
+13 -9
View File
@@ -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()
+25
View File
@@ -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
+24
View File
@@ -0,0 +1,24 @@
difficulty = 85
min_moves = 40
layout = """
#####
###### ########
## ####
### °E##
### ° °#####
##°°° ° °° ° ° °## ######
#°####### ## # # ### ######
# #° °°° ##
## ##°° ° ##°° ^###
# ###°° ####° ##
# #####^#####°° ##
# #####°####° #
# ° °°°°# #
# ° °°# #####^^##
## ° °###^########
## °# ##########
#S °##^#####
##########
"""
# Big_peanut avec des pièces partout (HAARRRRRR !)
+119 -4
View File
@@ -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
+12
View File
@@ -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
+23
View File
@@ -0,0 +1,23 @@
difficulty = 25
min_moves = 12
layout = """
########
#S ###
### ###
# ##
# ##
# ###
# ##
# °## ##
# ### ##
# ° #
# ^ #
#^## #° #
# ##### #
# ###### #
# ####### #
#^ ########^E#
###############
"""
# Sort of a jumping tutorial
+27
View File
@@ -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
+21
View File
@@ -0,0 +1,21 @@
difficulty = 10
min_moves = 14
layout = """
# E#
#°####
## #
### #
####°#
# ##
# ###
#°####
## #
### #
####°#
# ##
#S ###
######
"""
# Sort of a jumping tutorial
+19
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
difficulty = 7
min_moves = 11
layout = """
# #
#E S#
#########################
"""
# Teach to not only go right
+25
View File
@@ -0,0 +1,25 @@
difficulty = 40
min_moves = 8
layout = """
#^ ^#
#^ S^#
#^ #^#
#^ ^#
#^# ^#
#^ °^#
#^ #^#
#^# ^#
#^ #^#
#^# ^#
#^ ^#
#^ °^#
#^ #^#
#^ ^#
#^ ^#
#^ E^#
#^##^#
#^##^#
######
"""
# Needs to have learned to use left_down-right_down
+128
View File
@@ -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
+131
View File
@@ -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
+123
View File
@@ -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