Compare commits

..

No commits in common. "e1218b3eaa43deb9228c37eb863d04164cd9b37d" and "d1926c2b4da4f5ae62218196517aa54201f16145" have entirely different histories.

99 changed files with 875 additions and 1962 deletions

View file

@ -1,91 +0,0 @@
FROM node:20
ARG TZ
ENV TZ="$TZ"
ARG CLAUDE_CODE_VERSION=latest
# Install basic development tools and iptables/ipset
RUN apt-get update && apt-get install -y --no-install-recommends \
less \
git \
procps \
sudo \
fzf \
zsh \
man-db \
unzip \
gnupg2 \
gh \
iptables \
ipset \
iproute2 \
dnsutils \
aggregate \
jq \
nano \
vim \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Ensure default node user has access to /usr/local/share
RUN mkdir -p /usr/local/share/npm-global && \
chown -R node:node /usr/local/share
ARG USERNAME=node
# Persist bash history.
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
&& mkdir /commandhistory \
&& touch /commandhistory/.bash_history \
&& chown -R $USERNAME /commandhistory
# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true
# Create workspace and config directories and set permissions
RUN mkdir -p /workspace /home/node/.claude && \
chown -R node:node /workspace /home/node/.claude
WORKDIR /workspace
ARG GIT_DELTA_VERSION=0.18.2
RUN ARCH=$(dpkg --print-architecture) && \
wget "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
sudo dpkg -i "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" && \
rm "git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb"
# Set up non-root user
USER node
# Install global packages
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin
# Set the default shell to zsh rather than sh
ENV SHELL=/bin/zsh
# Set the default editor and visual
ENV EDITOR=nano
ENV VISUAL=nano
# Default powerline10k theme
ARG ZSH_IN_DOCKER_VERSION=1.2.0
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
-p git \
-p fzf \
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
-x
# Install Claude
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
# Copy and set up firewall script
COPY init-firewall.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/init-firewall.sh && \
echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
chmod 0440 /etc/sudoers.d/node-firewall
USER node

View file

@ -1,30 +0,0 @@
{
"name": "Claude Code Sandbox",
"build": {
"dockerfile": "Dockerfile",
"args": {
"TZ": "${localEnv:TZ:Europe/Paris}",
"CLAUDE_CODE_VERSION": "latest",
"GIT_DELTA_VERSION": "0.18.2",
"ZSH_IN_DOCKER_VERSION": "1.2.0"
}
},
"runArgs": [
"--cap-add=NET_ADMIN",
"--cap-add=NET_RAW"
],
"remoteUser": "node",
"mounts": [
"source=claude-code-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
"source=claude-code-config-${devcontainerId},target=/home/node/.claude,type=volume"
],
"containerEnv": {
"NODE_OPTIONS": "--max-old-space-size=4096",
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
"POWERLEVEL9K_DISABLE_GITSTATUS": "true"
},
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
"workspaceFolder": "/workspace",
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
"waitFor": "postStartCommand"
}

View file

@ -1,134 +0,0 @@
#!/bin/bash
set -euo pipefail # Exit on error, undefined vars, and pipeline failures
IFS=$'\n\t' # Stricter word splitting
# 1. Extract Docker DNS info BEFORE any flushing
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
# Flush existing rules and delete existing ipsets
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
ipset destroy allowed-domains 2>/dev/null || true
# 2. Selectively restore ONLY internal Docker DNS resolution
if [ -n "$DOCKER_DNS_RULES" ]; then
echo "Restoring Docker DNS rules..."
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
else
echo "No Docker DNS rules to restore"
fi
# First allow DNS and localhost before any restrictions
# Allow outbound DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Allow inbound DNS responses
iptables -A INPUT -p udp --sport 53 -j ACCEPT
# Allow outbound SSH
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
# Allow inbound SSH responses
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Create ipset with CIDR support
ipset create allowed-domains hash:net
# Fetch GitHub meta information and aggregate + add their IP ranges
echo "Fetching GitHub IP ranges..."
gh_ranges=$(curl -s https://api.github.com/meta)
if [ -z "$gh_ranges" ]; then
echo "ERROR: Failed to fetch GitHub IP ranges"
exit 1
fi
if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
echo "ERROR: GitHub API response missing required fields"
exit 1
fi
echo "Processing GitHub IPs..."
while read -r cidr; do
if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
exit 1
fi
echo "Adding GitHub range $cidr"
ipset add allowed-domains "$cidr"
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)
# Resolve and add other allowed domains
for domain in \
"registry.npmjs.org" \
"api.anthropic.com" \
"sentry.io" \
"statsig.anthropic.com" \
"statsig.com"; do
echo "Resolving $domain..."
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" {print $5}')
if [ -z "$ips" ]; then
echo "ERROR: Failed to resolve $domain"
exit 1
fi
while read -r ip; do
if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "ERROR: Invalid IP from DNS for $domain: $ip"
exit 1
fi
echo "Adding $ip for $domain"
ipset add allowed-domains "$ip"
done < <(echo "$ips")
done
# Get host IP from default route
HOST_IP=$(ip route | grep default | cut -d" " -f3)
if [ -z "$HOST_IP" ]; then
echo "ERROR: Failed to detect host IP"
exit 1
fi
HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
echo "Host network detected as: $HOST_NETWORK"
# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT
# Set default policies to DROP first
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# First allow established connections for already approved traffic
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Then allow only specific outbound traffic to allowed domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT
# Explicitly REJECT all other outbound traffic for immediate feedback
iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited
echo "Firewall configuration complete"
echo "Verifying firewall rules..."
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - was able to reach https://example.com"
exit 1
else
echo "Firewall verification passed - unable to reach https://example.com as expected"
fi
# Verify GitHub API access
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
exit 1
else
echo "Firewall verification passed - able to reach https://api.github.com as expected"
fi

1
.gitignore vendored
View file

@ -20,4 +20,3 @@ Thumbs.db
# Claude Code # Claude Code
.claude/ .claude/
.idea

View file

@ -1,26 +0,0 @@
# Chessistics
Jeu de logistique sur echiquier en Godot 4 / C#. Le joueur place des pieces d'echecs sur un plateau ; elles se deplacent automatiquement et transportent des ressources entre des productions et des demandes.
## Architecture : Black-Box Simulation
Ref: https://samuel-bouchet.fr/posts/2026-04-08-black-box-sim/
Le moteur de jeu (`chessistics-engine/`) est une boite noire sans aucune dependance vers Godot. Il recoit des **Commands**, mute son etat interne, et retourne des **Events**. Le code Godot (`Scripts/`) ne fait que traduire l'input en commands et les events en visuels/animations.
```
Input → Command → GameSim (state + rules) → Events → Presentation
```
- **Commands** (`PlacePieceCommand`, `StartSimulationCommand`, …) : seul moyen de modifier l'etat.
- **Events** (`PiecePlacedEvent`, `CargoDeliveredEvent`, …) : seul output du moteur. Le presenteur les consomme pour animer.
- **GameSim** : point d'entree unique. `ProcessCommand()` retourne la liste d'events.
- **Tests** : `chessistics-tests/` teste le moteur en headless, sans Godot.
## Pieges Godot a eviter
### MouseFilter sur les Controls enfants de Node2D
Tout `Control` (ColorRect, Label…) a `MouseFilter = Stop` par defaut. Quand un Control est enfant d'un Node2D (ex: les ColorRect dans CellView, les Labels dans PieceView), **il participe quand meme au systeme GUI et consomme les clics**, empechant `_UnhandledInput` de recevoir l'evenement.
**Regle** : toujours mettre `MouseFilter = Control.MouseFilterEnum.Ignore` sur les Controls purement visuels enfants de Node2D.

View file

@ -5,14 +5,13 @@
"width": 4, "width": 4,
"height": 4, "height": 4,
"productions": [ "productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" } { "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 }
], ],
"demands": [ "demands": [
{ "col": 3, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 } { "col": 3, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 }
], ],
"walls": [], "walls": [],
"stock": [ "stock": [
{ "kind": "pawn", "count": 4 }, { "kind": "rook", "count": 3 }
{ "kind": "rook", "count": 2 }
] ]
} }

View file

@ -5,7 +5,7 @@
"width": 6, "width": 6,
"height": 6, "height": 6,
"productions": [ "productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" } { "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 }
], ],
"demands": [ "demands": [
{ "col": 5, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 2, "deadline": 30 }, { "col": 5, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 2, "deadline": 30 },
@ -13,7 +13,6 @@
], ],
"walls": [], "walls": [],
"stock": [ "stock": [
{ "kind": "pawn", "count": 6 },
{ "kind": "rook", "count": 4 }, { "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 1 } { "kind": "bishop", "count": 1 }
] ]

View file

@ -5,8 +5,8 @@
"width": 6, "width": 6,
"height": 6, "height": 6,
"productions": [ "productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" }, { "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 },
{ "col": 5, "row": 0, "name": "Carriere", "cargo": "stone" } { "col": 5, "row": 0, "name": "Carriere", "cargo": "stone", "interval": 2 }
], ],
"demands": [ "demands": [
{ "col": 5, "row": 5, "name": "Depot Royal", "cargo": "wood", "amount": 2, "deadline": 40 }, { "col": 5, "row": 5, "name": "Depot Royal", "cargo": "wood", "amount": 2, "deadline": 40 },
@ -20,8 +20,7 @@
{ "col": 4, "row": 4 } { "col": 4, "row": 4 }
], ],
"stock": [ "stock": [
{ "kind": "pawn", "count": 6 }, { "kind": "rook", "count": 4 },
{ "kind": "rook", "count": 6 },
{ "kind": "bishop", "count": 1 }, { "kind": "bishop", "count": 1 },
{ "kind": "knight", "count": 2 } { "kind": "knight", "count": 2 }
] ]

View file

@ -1,27 +0,0 @@
{
"id": 4,
"name": "Le Carrefour",
"description": "Deux productions, deux demandes, et un carrefour au centre.",
"width": 8,
"height": 8,
"productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" },
{ "col": 7, "row": 7, "name": "Carriere", "cargo": "stone" }
],
"demands": [
{ "col": 7, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 40 },
{ "col": 0, "row": 7, "name": "Forge", "cargo": "stone", "amount": 3, "deadline": 40 }
],
"walls": [
{ "col": 3, "row": 3 },
{ "col": 4, "row": 4 },
{ "col": 3, "row": 4 },
{ "col": 4, "row": 3 }
],
"stock": [
{ "kind": "pawn", "count": 8 },
{ "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 },
{ "kind": "knight", "count": 2 }
]
}

View file

@ -1,35 +0,0 @@
{
"id": 5,
"name": "Le Labyrinthe",
"description": "Un couloir etroit serpente a travers les murs. Seuls les cavaliers peuvent prendre des raccourcis.",
"width": 8,
"height": 6,
"productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" },
{ "col": 0, "row": 5, "name": "Carriere", "cargo": "stone" }
],
"demands": [
{ "col": 7, "row": 5, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 50 },
{ "col": 7, "row": 0, "name": "Forge", "cargo": "stone", "amount": 3, "deadline": 50 }
],
"walls": [
{ "col": 2, "row": 0 },
{ "col": 2, "row": 1 },
{ "col": 2, "row": 2 },
{ "col": 2, "row": 3 },
{ "col": 4, "row": 2 },
{ "col": 4, "row": 3 },
{ "col": 4, "row": 4 },
{ "col": 4, "row": 5 },
{ "col": 6, "row": 0 },
{ "col": 6, "row": 1 },
{ "col": 6, "row": 2 },
{ "col": 6, "row": 3 }
],
"stock": [
{ "kind": "pawn", "count": 10 },
{ "kind": "rook", "count": 4 },
{ "kind": "bishop", "count": 2 },
{ "kind": "knight", "count": 3 }
]
}

View file

@ -1,35 +0,0 @@
{
"id": 6,
"name": "Trois Royaumes",
"description": "Trois productions, trois demandes. Gerez un reseau complet sans interferences.",
"width": 10,
"height": 8,
"productions": [
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" },
{ "col": 0, "row": 7, "name": "Carriere", "cargo": "stone" },
{ "col": 9, "row": 3, "name": "Scierie Est", "cargo": "wood" }
],
"demands": [
{ "col": 9, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 50 },
{ "col": 9, "row": 7, "name": "Forge", "cargo": "stone", "amount": 3, "deadline": 50 },
{ "col": 4, "row": 7, "name": "Chantier", "cargo": "wood", "amount": 3, "deadline": 50 }
],
"walls": [
{ "col": 3, "row": 2 },
{ "col": 3, "row": 3 },
{ "col": 3, "row": 4 },
{ "col": 3, "row": 5 },
{ "col": 6, "row": 2 },
{ "col": 6, "row": 3 },
{ "col": 6, "row": 4 },
{ "col": 6, "row": 5 },
{ "col": 4, "row": 3 },
{ "col": 5, "row": 3 }
],
"stock": [
{ "kind": "pawn", "count": 14 },
{ "kind": "rook", "count": 6 },
{ "kind": "bishop", "count": 3 },
{ "kind": "knight", "count": 4 }
]
}

24
PLAN.md
View file

@ -20,13 +20,25 @@
- GDD stock corrections: Level 2 = 6R+1B, Level 3 = 10R+2K - GDD stock corrections: Level 2 = 6R+1B, Level 3 = 10R+2K
- 60 tests passing including 2 new CargoFilter tests - 60 tests passing including 2 new CargoFilter tests
## Phase 3: Pion, surplus stock, levels 4-6 (DONE) ## Phase 3: Surplus stock and puzzle difficulty tuning
- Pion: orthogonal range 1, status 1 (lowest), cheap relay maillon **Goal**: Levels give more pieces than the minimum, creating genuine puzzle space.
- Surplus stock on all levels (more pieces than minimum solution)
- Levels 4-6: Le Carrefour (8x8), Le Labyrinthe (8x6), Trois Royaumes (10x8) - With forward-preferring transfers working, longer chains are viable.
- Production interval removed: all productions fire every turn - Design levels where the player has choice: multiple valid solutions with different
- GDD updated with Pion, 6 levels efficiency scores (PiecesUsed, TurnsTaken, CellsOccupied).
- Add scoring/star system based on Metrics.
- Levels 4-6: increasing board size (8x8, 10x10), more complex wall layouts, multiple
productions and demands.
## Phase 4: New piece — Pion (Pawn)
**Goal**: Add a one-directional piece for asymmetric relay constraints.
- Pion moves forward only (one direction, range 1).
- Cheap to place (low piece cost if scoring is added).
- Creates interesting constraints: must plan direction of cargo flow.
- Test levels specifically designed around Pion usage.
## Phase 5: Network levels and Dame (Queen) ## Phase 5: Network levels and Dame (Queen)

View file

@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://6j24v4md60t7"] [gd_scene load_steps=2 format=3 uid="uid://main_scene"]
[ext_resource type="Script" uid="uid://dygonjc0xhp15" path="res://Scripts/Main.cs" id="1"] [ext_resource type="Script" path="res://Scripts/Main.cs" id="1"]
[node name="Main" type="Node2D" unique_id=2090714159] [node name="Main" type="Node2D"]
script = ExtResource("1") script = ExtResource("1")

View file

@ -6,93 +6,82 @@ namespace Chessistics.Scripts.Board;
public partial class BoardView : Node2D public partial class BoardView : Node2D
{ {
public const int CellSize = 80; public const int CellSize = 80;
private readonly Dictionary<Coords, CellView> _cells = new(); private readonly Dictionary<Coords, CellView> _cells = new();
private int _width; private int _width;
private int _height; private int _height;
public void BuildBoard(LevelDef level) public void BuildBoard(LevelDef level)
{ {
// Clear existing children // Clear existing children
foreach (var child in GetChildren()) foreach (var child in GetChildren())
child.QueueFree(); child.QueueFree();
_cells.Clear(); _cells.Clear();
_width = level.Width; _width = level.Width;
_height = level.Height; _height = level.Height;
var boardState = BoardState.FromLevel(level); var boardState = BoardState.FromLevel(level);
for (int col = 0; col < level.Width; col++) for (int col = 0; col < level.Width; col++)
{ {
for (int row = 0; row < level.Height; row++) for (int row = 0; row < level.Height; row++)
{ {
var coords = new Coords(col, row); var coords = new Coords(col, row);
var cellView = new CellView(); var cellView = new CellView();
cellView.Setup(coords, boardState.GetCell(coords), CellSize); cellView.Setup(coords, boardState.GetCell(coords), CellSize);
AddChild(cellView); AddChild(cellView);
_cells[coords] = cellView; _cells[coords] = cellView;
} }
} }
// Label productions and demands // Label productions and demands
foreach (var prod in level.Productions) foreach (var prod in level.Productions)
{ {
if (_cells.TryGetValue(prod.Position, out var cell)) if (_cells.TryGetValue(prod.Position, out var cell))
cell.SetLabel(prod.Name); cell.SetLabel(prod.Name);
} }
foreach (var demand in level.Demands) foreach (var demand in level.Demands)
{ {
if (_cells.TryGetValue(demand.Position, out var cell)) if (_cells.TryGetValue(demand.Position, out var cell))
cell.SetLabel(demand.Name); cell.SetLabel(demand.Name);
} }
} }
public Coords? PixelToCoords(Vector2 localPos) public Coords? PixelToCoords(Vector2 localPos)
{ {
int col = Mathf.FloorToInt(localPos.X / CellSize); int col = Mathf.FloorToInt(localPos.X / CellSize);
// Cell at row R has top-left Y = -R*CellSize, extending downward. int row = Mathf.FloorToInt(-localPos.Y / CellSize);
// floor(-Y/Size) != -floor(Y/Size) for non-integers, so use the latter.
int row = -Mathf.FloorToInt(localPos.Y / CellSize);
var coords = new Coords(col, row); var coords = new Coords(col, row);
return coords.IsOnBoard(_width, _height) ? coords : null; return coords.IsOnBoard(_width, _height) ? coords : null;
} }
public Vector2 CoordsToPixel(Coords coords) public Vector2 CoordsToPixel(Coords coords)
{ {
return new Vector2( return new Vector2(
coords.Col * CellSize + CellSize / 2f, coords.Col * CellSize + CellSize / 2f,
-coords.Row * CellSize + CellSize / 2f -coords.Row * CellSize + CellSize / 2f
); );
} }
public CellView? GetCellView(Coords coords) public CellView? GetCellView(Coords coords)
=> _cells.GetValueOrDefault(coords); => _cells.GetValueOrDefault(coords);
public void SetHoverCell(Coords? coords) public void ClearHighlights()
{ {
foreach (var cell in _cells.Values) foreach (var cell in _cells.Values)
cell.SetHover(false); cell.SetHighlight(false);
}
if (coords != null && _cells.TryGetValue(coords.Value, out var cellView)) public void HighlightCells(IEnumerable<Coords> cells, Color color)
cellView.SetHover(true); {
} foreach (var coords in cells)
{
public void ClearHighlights() if (_cells.TryGetValue(coords, out var cellView))
{ cellView.SetHighlightColor(color);
foreach (var cell in _cells.Values) }
cell.SetHighlight(false); }
}
public void HighlightCells(IEnumerable<Coords> cells, Color color)
{
foreach (var coords in cells)
{
if (_cells.TryGetValue(coords, out var cellView))
cellView.SetHighlightColor(color);
}
}
} }

View file

@ -1 +0,0 @@
uid://njfxxe08s5w

View file

@ -9,12 +9,6 @@ public partial class CellView : Node2D
private ColorRect _highlight = null!; private ColorRect _highlight = null!;
private Label _label = null!; private Label _label = null!;
// Hover outline (4 thin rects forming a border)
private ColorRect _hoverTop = null!;
private ColorRect _hoverBottom = null!;
private ColorRect _hoverLeft = null!;
private ColorRect _hoverRight = null!;
public Coords Coords { get; private set; } public Coords Coords { get; private set; }
private static readonly Color LightColor = new("#F0D9B5"); private static readonly Color LightColor = new("#F0D9B5");
@ -23,9 +17,6 @@ public partial class CellView : Node2D
private static readonly Color ProductionColor = new("#6B8E5A"); private static readonly Color ProductionColor = new("#6B8E5A");
private static readonly Color DemandColor = new("#C9A833"); private static readonly Color DemandColor = new("#C9A833");
private static readonly Color HighlightColor = new("#44FF4444"); private static readonly Color HighlightColor = new("#44FF4444");
private static readonly Color HoverOutlineColor = new("#FFFFFFAA");
private const int OutlineWidth = 2;
public void Setup(Coords coords, CellType cellType, int cellSize) public void Setup(Coords coords, CellType cellType, int cellSize)
{ {
@ -35,8 +26,7 @@ public partial class CellView : Node2D
_background = new ColorRect _background = new ColorRect
{ {
Size = new Vector2(cellSize, cellSize), Size = new Vector2(cellSize, cellSize),
Position = Vector2.Zero, Position = Vector2.Zero
MouseFilter = Control.MouseFilterEnum.Ignore
}; };
var baseColor = coords.IsLight ? LightColor : DarkColor; var baseColor = coords.IsLight ? LightColor : DarkColor;
@ -54,57 +44,14 @@ public partial class CellView : Node2D
Size = new Vector2(cellSize, cellSize), Size = new Vector2(cellSize, cellSize),
Position = Vector2.Zero, Position = Vector2.Zero,
Color = HighlightColor, Color = HighlightColor,
Visible = false, Visible = false
MouseFilter = Control.MouseFilterEnum.Ignore
}; };
AddChild(_highlight); AddChild(_highlight);
// Hover outline (4 border rects)
_hoverTop = new ColorRect
{
Size = new Vector2(cellSize, OutlineWidth),
Position = Vector2.Zero,
Color = HoverOutlineColor,
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_hoverTop);
_hoverBottom = new ColorRect
{
Size = new Vector2(cellSize, OutlineWidth),
Position = new Vector2(0, cellSize - OutlineWidth),
Color = HoverOutlineColor,
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_hoverBottom);
_hoverLeft = new ColorRect
{
Size = new Vector2(OutlineWidth, cellSize),
Position = Vector2.Zero,
Color = HoverOutlineColor,
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_hoverLeft);
_hoverRight = new ColorRect
{
Size = new Vector2(OutlineWidth, cellSize),
Position = new Vector2(cellSize - OutlineWidth, 0),
Color = HoverOutlineColor,
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore
};
AddChild(_hoverRight);
_label = new Label _label = new Label
{ {
Position = new Vector2(2, 2), Position = new Vector2(2, 2),
Text = "", Text = "",
MouseFilter = Control.MouseFilterEnum.Ignore
}; };
_label.AddThemeFontSizeOverride("font_size", 10); _label.AddThemeFontSizeOverride("font_size", 10);
AddChild(_label); AddChild(_label);
@ -113,29 +60,9 @@ public partial class CellView : Node2D
public void SetLabel(string text) => _label.Text = text; public void SetLabel(string text) => _label.Text = text;
public void SetHighlight(bool on) => _highlight.Visible = on; public void SetHighlight(bool on) => _highlight.Visible = on;
public void SetHover(bool on)
{
_hoverTop.Visible = on;
_hoverBottom.Visible = on;
_hoverLeft.Visible = on;
_hoverRight.Visible = on;
}
public void SetHighlightColor(Color color) public void SetHighlightColor(Color color)
{ {
_highlight.Color = color; _highlight.Color = color;
_highlight.Visible = true; _highlight.Visible = true;
} }
/// <summary>
/// Brief white flash on the cell to signal production.
/// </summary>
public void FlashProduce(float duration = 0.3f)
{
_highlight.Color = new Color(1, 1, 1, 0.5f);
_highlight.Visible = true;
var tween = CreateTween();
tween.TweenProperty(_highlight, "color", new Color(1, 1, 1, 0f), duration);
tween.TweenCallback(Callable.From(() => _highlight.Visible = false));
}
} }

View file

@ -1 +0,0 @@
uid://dt08gv2w0t3kj

View file

@ -24,7 +24,6 @@ public partial class InputMapper : Node
private Coords? _selectedStart; private Coords? _selectedStart;
private PlacementPhase _phase = PlacementPhase.None; private PlacementPhase _phase = PlacementPhase.None;
private BoardSnapshot? _snapshot; private BoardSnapshot? _snapshot;
private Coords? _hoverCoords;
public PlacementPhase CurrentPhase => _phase; public PlacementPhase CurrentPhase => _phase;
@ -33,15 +32,10 @@ public partial class InputMapper : Node
_boardView = boardView; _boardView = boardView;
} }
public void SetSnapshot(BoardSnapshot snapshot) public void SetSnapshot(BoardSnapshot snapshot) => _snapshot = snapshot;
{
GD.Print($"[InputMapper] SetSnapshot called — null? {snapshot == null}");
_snapshot = snapshot;
}
public void SelectPieceKind(PieceKind kind) public void SelectPieceKind(PieceKind kind)
{ {
GD.Print($"[InputMapper] SelectPieceKind: {kind}, phase → SelectingStart");
_selectedKind = kind; _selectedKind = kind;
_selectedStart = null; _selectedStart = null;
_phase = PlacementPhase.SelectingStart; _phase = PlacementPhase.SelectingStart;
@ -56,20 +50,6 @@ public partial class InputMapper : Node
EmitSignal(SignalName.Cancelled); EmitSignal(SignalName.Cancelled);
} }
public override void _Process(double delta)
{
if (_boardView == null || !_boardView.Visible) return;
var localPos = _boardView.GetLocalMousePosition();
var coords = _boardView.PixelToCoords(localPos);
if (coords != _hoverCoords)
{
_hoverCoords = coords;
_boardView.SetHoverCell(coords);
}
}
public override void _UnhandledInput(InputEvent @event) public override void _UnhandledInput(InputEvent @event)
{ {
if (@event is InputEventMouseButton mouseEvent && mouseEvent.Pressed) if (@event is InputEventMouseButton mouseEvent && mouseEvent.Pressed)
@ -82,9 +62,7 @@ public partial class InputMapper : Node
if (mouseEvent.ButtonIndex == MouseButton.Left) if (mouseEvent.ButtonIndex == MouseButton.Left)
{ {
var localPos = _boardView.GetLocalMousePosition(); HandleLeftClick(mouseEvent.GlobalPosition);
GD.Print($"[InputMapper] LEFT CLICK — localPos={localPos}, phase={_phase}");
HandleLeftClick();
} }
} }
@ -94,18 +72,12 @@ public partial class InputMapper : Node
} }
} }
private void HandleLeftClick() private void HandleLeftClick(Vector2 globalPos)
{ {
var localPos = _boardView.GetLocalMousePosition(); var localPos = _boardView.ToLocal(globalPos);
var coords = _boardView.PixelToCoords(localPos); var coords = _boardView.PixelToCoords(localPos);
GD.Print($"[InputMapper] HandleLeftClick — localPos={localPos}, coords={coords}"); if (coords == null) return;
if (coords == null)
{
GD.Print("[InputMapper] coords is null — click outside board");
return;
}
switch (_phase) switch (_phase)
{ {
@ -125,22 +97,13 @@ public partial class InputMapper : Node
private void OnStartSelected(Coords start) private void OnStartSelected(Coords start)
{ {
if (_selectedKind == null || _snapshot == null) if (_selectedKind == null || _snapshot == null) return;
{
GD.Print($"[InputMapper] OnStartSelected ABORT — kind={_selectedKind}, snapshot={(_snapshot != null ? "ok" : "null")}");
return;
}
// Build a temporary board state for move validation
var boardState = GetBoardStateForValidation(); var boardState = GetBoardStateForValidation();
if (boardState == null) if (boardState == null) return;
{
GD.Print("[InputMapper] OnStartSelected ABORT — boardState is null");
return;
}
var legalEnds = MoveValidator.GetLegalEndCells(_selectedKind.Value, start, boardState); var legalEnds = MoveValidator.GetLegalEndCells(_selectedKind.Value, start, boardState);
GD.Print($"[InputMapper] OnStartSelected({start}) — {legalEnds.Count} legal end cells");
if (legalEnds.Count == 0) return; if (legalEnds.Count == 0) return;
_selectedStart = start; _selectedStart = start;
@ -159,7 +122,6 @@ public partial class InputMapper : Node
var start = _selectedStart.Value; var start = _selectedStart.Value;
var kind = _selectedKind.Value; var kind = _selectedKind.Value;
GD.Print($"[InputMapper] OnEndSelected — placing {kind} from {start} to {end}");
EmitSignal(SignalName.PlacementRequested, (int)kind, start.Col, start.Row, end.Col, end.Row); EmitSignal(SignalName.PlacementRequested, (int)kind, start.Col, start.Row, end.Col, end.Row);
// Reset placement state // Reset placement state
@ -171,8 +133,12 @@ public partial class InputMapper : Node
private BoardState? GetBoardStateForValidation() private BoardState? GetBoardStateForValidation()
{ {
// Reconstruct a minimal BoardState from snapshot for MoveValidator
// This is a read-only usage — we just need the grid and dimensions
if (_snapshot == null) return null; if (_snapshot == null) return null;
// We need a LevelDef-like structure to create a BoardState
// For validation purposes, we create a fresh one from the snapshot data
var level = new LevelDef var level = new LevelDef
{ {
Width = _snapshot.Width, Width = _snapshot.Width,
@ -185,6 +151,7 @@ public partial class InputMapper : Node
var state = BoardState.FromLevel(level); var state = BoardState.FromLevel(level);
// Copy grid from snapshot
for (int c = 0; c < _snapshot.Width; c++) for (int c = 0; c < _snapshot.Width; c++)
for (int r = 0; r < _snapshot.Height; r++) for (int r = 0; r < _snapshot.Height; r++)
state.Grid[c, r] = _snapshot.Grid[c, r]; state.Grid[c, r] = _snapshot.Grid[c, r];

View file

@ -1 +0,0 @@
uid://cfpek0cba5h50

View file

@ -1,6 +1,5 @@
using Godot; using Godot;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Chessistics.Engine.Commands; using Chessistics.Engine.Commands;
using Chessistics.Engine.Events; using Chessistics.Engine.Events;
using Chessistics.Engine.Loading; using Chessistics.Engine.Loading;
@ -16,500 +15,424 @@ namespace Chessistics.Scripts;
public partial class Main : Node2D public partial class Main : Node2D
{ {
private GameSim? _sim; private GameSim? _sim;
private LevelDef? _currentLevel; private LevelDef? _currentLevel;
private int _currentLevelIndex; private int _currentLevelIndex;
// Views // Views
private BoardView _boardView = null!; private BoardView _boardView = null!;
private InputMapper _inputMapper = null!; private InputMapper _inputMapper = null!;
private EventAnimator _eventAnimator = null!; private EventAnimator _eventAnimator = null!;
// UI // UI
private CanvasLayer _uiLayer = null!; private CanvasLayer _uiLayer = null!;
private ObjectivePanel _objectivePanel = null!; private ObjectivePanel _objectivePanel = null!;
private PieceStockPanel _pieceStockPanel = null!; private PieceStockPanel _pieceStockPanel = null!;
private DetailPanel _detailPanel = null!; private DetailPanel _detailPanel = null!;
private ControlBar _controlBar = null!; private ControlBar _controlBar = null!;
private MetricsOverlay _metricsOverlay = null!; private MetricsOverlay _metricsOverlay = null!;
private LevelSelectScreen _levelSelectScreen = null!; private LevelSelectScreen _levelSelectScreen = null!;
private Label _levelTitle = null!; private Label _levelTitle = null!;
private PanelContainer _sidePanel = null!;
private PanelContainer _controlBarWrapper = null!; // Simulation timer
private Camera2D _camera = null!; private Godot.Timer _simTimer = null!;
private float _simInterval = 1.0f;
// Simulation timer private bool _running;
private Godot.Timer _simTimer = null!;
private float _simInterval = 1.0f; private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json"];
private bool _running;
private bool _panning; private static readonly Color BackgroundColor = new("#2D2D2D");
private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json", "level_04.json", "level_05.json", "level_06.json"]; public override void _Ready()
{
private const float SidePanelWidth = 280f; RenderingServer.SetDefaultClearColor(BackgroundColor);
private const float ControlBarHeight = 48f;
BuildSceneTree();
private static readonly Color BackgroundColor = new("#2D2D2D"); ConnectSignals();
ShowLevelSelect();
public override void _Ready() }
{
RenderingServer.SetDefaultClearColor(BackgroundColor); private void BuildSceneTree()
{
BuildSceneTree(); // Camera
ConnectSignals(); var camera = new Camera2D { Enabled = true };
ShowLevelSelect(); AddChild(camera);
}
// Board
public override void _UnhandledInput(InputEvent @event) _boardView = new BoardView();
{ AddChild(_boardView);
if (@event is InputEventMouseButton mb)
{ // Input
if (mb.ButtonIndex == MouseButton.Middle) _inputMapper = new InputMapper();
_panning = mb.Pressed; _inputMapper.Initialize(_boardView);
} AddChild(_inputMapper);
else if (@event is InputEventMouseMotion motion && _panning)
{ // Animator
_camera.Position -= motion.Relative / _camera.Zoom; _eventAnimator = new EventAnimator();
} AddChild(_eventAnimator);
}
// Sim timer
private void BuildSceneTree() _simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval };
{ _simTimer.Timeout += OnSimTimerTick;
// Camera AddChild(_simTimer);
_camera = new Camera2D { Enabled = true };
AddChild(_camera); // UI Layer
_uiLayer = new CanvasLayer();
// Board AddChild(_uiLayer);
_boardView = new BoardView();
AddChild(_boardView); // Level title
_levelTitle = new Label
// Input {
_inputMapper = new InputMapper(); Position = new Vector2(10, 10),
_inputMapper.Initialize(_boardView); Text = "CHESSISTICS"
AddChild(_inputMapper); };
_levelTitle.AddThemeFontSizeOverride("font_size", 20);
// Animator _uiLayer.AddChild(_levelTitle);
_eventAnimator = new EventAnimator();
AddChild(_eventAnimator); // Side panel (right)
var sidePanel = new VBoxContainer
// Sim timer {
_simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval }; Position = new Vector2(700, 50),
_simTimer.Timeout += OnSimTimerTick; CustomMinimumSize = new Vector2(200, 500)
AddChild(_simTimer); };
// --- UI Layer --- _objectivePanel = new ObjectivePanel();
_uiLayer = new CanvasLayer(); sidePanel.AddChild(_objectivePanel);
AddChild(_uiLayer); sidePanel.AddChild(new HSeparator());
// Root control anchored to viewport (required for child anchoring) _pieceStockPanel = new PieceStockPanel();
var uiRoot = new Control(); sidePanel.AddChild(_pieceStockPanel);
uiRoot.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore; _detailPanel = new DetailPanel();
_uiLayer.AddChild(uiRoot); sidePanel.AddChild(_detailPanel);
// Level title (top-left) _uiLayer.AddChild(sidePanel);
_levelTitle = new Label { Text = "CHESSISTICS" };
_levelTitle.SetAnchorsPreset(Control.LayoutPreset.TopLeft); // Control bar (bottom)
_levelTitle.OffsetLeft = 16; _controlBar = new ControlBar
_levelTitle.OffsetTop = 12; {
_levelTitle.AddThemeFontSizeOverride("font_size", 20); Position = new Vector2(10, 600)
_levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore; };
uiRoot.AddChild(_levelTitle); _uiLayer.AddChild(_controlBar);
// --- Side Panel (anchored to right edge) --- // Metrics overlay (center)
_sidePanel = new PanelContainer(); _metricsOverlay = new MetricsOverlay
_sidePanel.AnchorLeft = 1.0f; {
_sidePanel.AnchorRight = 1.0f; Position = new Vector2(200, 150),
_sidePanel.AnchorTop = 0.0f; CustomMinimumSize = new Vector2(300, 250)
_sidePanel.AnchorBottom = 1.0f; };
_sidePanel.OffsetLeft = -SidePanelWidth; _uiLayer.AddChild(_metricsOverlay);
_sidePanel.OffsetRight = 0;
_sidePanel.OffsetTop = 0; // Level select screen
_sidePanel.OffsetBottom = -ControlBarHeight; _levelSelectScreen = new LevelSelectScreen();
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
var sidePanelStyle = new StyleBoxFlat _uiLayer.AddChild(_levelSelectScreen);
{
BgColor = new Color(0.14f, 0.14f, 0.16f, 0.97f), // Initialize animator
BorderColor = new Color(0.25f, 0.25f, 0.28f), _eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
BorderWidthLeft = 1, }
ContentMarginLeft = 16,
ContentMarginRight = 16, private void ConnectSignals()
ContentMarginTop = 16, {
ContentMarginBottom = 16 _levelSelectScreen.LevelSelected += OnLevelSelected;
}; _pieceStockPanel.PieceSelected += OnPieceKindSelected;
_sidePanel.AddThemeStyleboxOverride("panel", sidePanelStyle); _inputMapper.PlacementRequested += OnPlacementRequested;
_inputMapper.Cancelled += OnPlacementCancelled;
var sideScroll = new ScrollContainer _controlBar.PlayPressed += OnPlay;
{ _controlBar.PausePressed += OnPause;
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled, _controlBar.StepPressed += OnStep;
SizeFlagsVertical = Control.SizeFlags.ExpandFill _controlBar.StopPressed += OnStop;
}; _controlBar.SpeedChanged += OnSpeedChanged;
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
var sideVBox = new VBoxContainer(); _eventAnimator.VictoryReached += OnVictory;
sideVBox.AddThemeConstantOverride("separation", 12); _eventAnimator.CollisionOccurred += OnCollision;
_metricsOverlay.RetryPressed += OnRetry;
_objectivePanel = new ObjectivePanel(); _metricsOverlay.NextLevelPressed += OnNextLevel;
sideVBox.AddChild(_objectivePanel); _detailPanel.RemoveRequested += OnRemoveRequested;
sideVBox.AddChild(new HSeparator()); _inputMapper.CellClicked += OnCellClicked;
}
_pieceStockPanel = new PieceStockPanel();
sideVBox.AddChild(_pieceStockPanel); private void OnCellClicked(int col, int row)
{
_detailPanel = new DetailPanel(); if (_sim == null) return;
sideVBox.AddChild(_detailPanel); var snap = _sim.GetSnapshot();
if (snap.Phase != SimPhase.Edit) return;
sideScroll.AddChild(sideVBox);
_sidePanel.AddChild(sideScroll); var coords = new Coords(col, row);
uiRoot.AddChild(_sidePanel); var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
if (piece != null)
// --- Control Bar (anchored to bottom, left of side panel) --- _detailPanel.ShowPiece(piece);
_controlBarWrapper = new PanelContainer(); else
_controlBarWrapper.AnchorLeft = 0.0f; _detailPanel.Hide();
_controlBarWrapper.AnchorRight = 1.0f; }
_controlBarWrapper.AnchorTop = 1.0f;
_controlBarWrapper.AnchorBottom = 1.0f; // --- Level Management ---
_controlBarWrapper.OffsetTop = -ControlBarHeight;
_controlBarWrapper.OffsetRight = -SidePanelWidth; private void ShowLevelSelect()
{
var controlBarStyle = new StyleBoxFlat _levelSelectScreen.Visible = true;
{ _boardView.Visible = false;
BgColor = new Color(0.11f, 0.11f, 0.13f, 0.97f), }
BorderColor = new Color(0.25f, 0.25f, 0.28f),
BorderWidthTop = 1, private void OnLevelSelected(int levelIndex)
ContentMarginLeft = 12, {
ContentMarginRight = 12, _currentLevelIndex = levelIndex;
ContentMarginTop = 4, LoadLevel(levelIndex);
ContentMarginBottom = 4 }
};
_controlBarWrapper.AddThemeStyleboxOverride("panel", controlBarStyle); private void LoadLevel(int index)
{
_controlBar = new ControlBar(); if (index < 0 || index >= LevelFiles.Length) return;
_controlBarWrapper.AddChild(_controlBar);
uiRoot.AddChild(_controlBarWrapper); var path = $"res://Data/levels/{LevelFiles[index]}";
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read);
// --- Metrics Overlay (centered in board area) --- if (file == null)
var metricsCenter = new CenterContainer(); {
metricsCenter.SetAnchorsPreset(Control.LayoutPreset.FullRect); GD.PrintErr($"Cannot open level file: {path}");
metricsCenter.OffsetRight = -SidePanelWidth; return;
metricsCenter.OffsetBottom = -ControlBarHeight; }
metricsCenter.MouseFilter = Control.MouseFilterEnum.Ignore;
var json = file.GetAsText();
_metricsOverlay = new MetricsOverlay(); file.Close();
_metricsOverlay.CustomMinimumSize = new Vector2(340, 260);
metricsCenter.AddChild(_metricsOverlay); _currentLevel = LevelLoader.Load(json);
uiRoot.AddChild(metricsCenter); _sim = new GameSim(_currentLevel);
// --- Level Select Screen (full viewport) --- _levelSelectScreen.Visible = false;
_levelSelectScreen = new LevelSelectScreen(); _boardView.Visible = true;
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
uiRoot.AddChild(_levelSelectScreen); _boardView.BuildBoard(_currentLevel);
_objectivePanel.Setup(_currentLevel.Demands);
// Initialize animator _pieceStockPanel.Setup(_currentLevel.Stock);
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay); _controlBar.UpdateForPhase(SimPhase.Edit);
} _controlBar.ResetTurn();
_metricsOverlay.Hide();
private void ConnectSignals() _detailPanel.Hide();
{ _eventAnimator.ClearAll();
_levelSelectScreen.LevelSelected += OnLevelSelected;
_pieceStockPanel.PieceSelected += OnPieceKindSelected; _levelTitle.Text = $"CHESSISTICS — {_currentLevel.Name}";
_inputMapper.PlacementRequested += OnPlacementRequested;
_inputMapper.Cancelled += OnPlacementCancelled; // Center camera on board
_controlBar.PlayPressed += OnPlay; var cam = GetNode<Camera2D>("Camera2D");
_controlBar.PausePressed += OnPause; cam.Position = new Vector2(
_controlBar.StepPressed += OnStep; _currentLevel.Width * BoardView.CellSize / 2f,
_controlBar.StopPressed += OnStop; -_currentLevel.Height * BoardView.CellSize / 2f
_controlBar.SpeedChanged += OnSpeedChanged; );
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
_eventAnimator.VictoryReached += OnVictory; _inputMapper.SetSnapshot(_sim.GetSnapshot());
_metricsOverlay.RetryPressed += OnRetry; }
_metricsOverlay.NextLevelPressed += OnNextLevel;
_detailPanel.RemoveRequested += OnRemoveRequested; // --- Edit Phase ---
_inputMapper.CellClicked += OnCellClicked;
} private void OnPieceKindSelected(int kindIndex)
{
private void OnCellClicked(int col, int row) _inputMapper.SelectPieceKind((PieceKind)kindIndex);
{ }
if (_sim == null) return;
var snap = _sim.GetSnapshot(); private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow)
if (snap.Phase != SimPhase.Edit) return; {
if (_sim == null) return;
var coords = new Coords(col, row);
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords); var kind = (PieceKind)kindIndex;
if (piece != null) var start = new Coords(startCol, startRow);
_detailPanel.ShowPiece(piece); var end = new Coords(endCol, endRow);
else
_detailPanel.Hide(); var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end));
} HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
// --- Level Management --- }
private void ShowLevelSelect() private void OnPlacementCancelled()
{ {
_levelSelectScreen.Visible = true; _pieceStockPanel.ClearSelection();
_boardView.Visible = false; }
_sidePanel.Visible = false;
_controlBarWrapper.Visible = false; private void OnRemoveRequested(int pieceId)
_levelTitle.Visible = false; {
} if (_sim == null) return;
private void OnLevelSelected(int levelIndex) var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId));
{ HandleEditEvents(events);
_currentLevelIndex = levelIndex; _inputMapper.SetSnapshot(_sim.GetSnapshot());
LoadLevel(levelIndex); }
}
private void HandleEditEvents(IReadOnlyList<IWorldEvent> events)
private void LoadLevel(int index) {
{ foreach (var evt in events)
if (index < 0 || index >= LevelFiles.Length) return; {
switch (evt)
var path = $"res://Data/levels/{LevelFiles[index]}"; {
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read); case PiecePlacedEvent placed:
if (file == null) CreatePieceVisual(placed);
{ UpdateStockFromSnapshot();
GD.PrintErr($"Cannot open level file: {path}"); break;
return;
} case PieceRemovedEvent removed:
_eventAnimator.UnregisterPiece(removed.PieceId);
var json = file.GetAsText(); UpdateStockFromSnapshot();
file.Close(); _detailPanel.Hide();
break;
_currentLevel = LevelLoader.Load(json);
_sim = new GameSim(_currentLevel); case PlacementRejectedEvent rejected:
GD.Print($"Placement rejected: {rejected.Reason}");
_levelSelectScreen.Visible = false; break;
_boardView.Visible = true;
_sidePanel.Visible = true; case CommandRejectedEvent rejected:
_controlBarWrapper.Visible = true; GD.Print($"Command rejected: {rejected.Reason}");
_levelTitle.Visible = true; break;
}
_boardView.BuildBoard(_currentLevel); }
_objectivePanel.Setup(_currentLevel.Demands); }
_pieceStockPanel.Setup(_currentLevel.Stock);
_controlBar.UpdateForPhase(SimPhase.Edit); private void CreatePieceVisual(PiecePlacedEvent placed)
_controlBar.ResetTurn(); {
_metricsOverlay.Hide(); var pieceView = new PieceView();
_detailPanel.Hide(); pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
_eventAnimator.ClearAll(); _boardView.AddChild(pieceView);
_levelTitle.Text = $"CHESSISTICS — {_currentLevel.Name}"; var color = placed.Kind switch
{
// Center camera on board PieceKind.Rook => new Color("#4A7AB5"),
// Board X spans 0..W*Cell, Y spans -(H-1)*Cell .. Cell PieceKind.Bishop => new Color("#B54A8E"),
_camera.Position = new Vector2( PieceKind.Knight => new Color("#B5824A"),
_currentLevel.Width * BoardView.CellSize / 2f, _ => Colors.White
-_currentLevel.Height * BoardView.CellSize / 2f + BoardView.CellSize };
);
_camera.Offset = new Vector2(-SidePanelWidth / 2f, -ControlBarHeight / 2f); var trajectView = new TrajectView();
trajectView.Setup(placed.PieceId,
var snapshot = _sim.GetSnapshot(); _boardView.CoordsToPixel(placed.Start),
GD.Print($"[Main] LoadLevel — snapshot null? {snapshot == null}, width={snapshot?.Width}, height={snapshot?.Height}"); _boardView.CoordsToPixel(placed.End),
_inputMapper.SetSnapshot(snapshot); color);
} _boardView.AddChild(trajectView);
// --- Edit Phase --- _eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView);
}
private void OnPieceKindSelected(int kindIndex)
{ private void UpdateStockFromSnapshot()
_inputMapper.SelectPieceKind((PieceKind)kindIndex); {
} if (_sim == null) return;
var snap = _sim.GetSnapshot();
private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow) foreach (var (kind, remaining) in snap.RemainingStock)
{ _pieceStockPanel.UpdateCount(kind, remaining);
if (_sim == null) return; }
var kind = (PieceKind)kindIndex; // --- Exec Phase ---
var start = new Coords(startCol, startRow);
var end = new Coords(endCol, endRow); private void OnPlay()
{
var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end)); if (_sim == null) return;
HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot()); var snap = _sim.GetSnapshot();
} if (snap.Phase == SimPhase.Edit)
{
private void OnPlacementCancelled() var events = _sim.ProcessCommand(new StartSimulationCommand());
{ foreach (var evt in events)
_pieceStockPanel.ClearSelection(); {
} if (evt is CommandRejectedEvent r)
{
private void OnRemoveRequested(int pieceId) GD.Print($"Cannot start: {r.Reason}");
{ return;
if (_sim == null) return; }
}
var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId)); }
HandleEditEvents(events); else if (snap.Phase == SimPhase.Paused)
_inputMapper.SetSnapshot(_sim.GetSnapshot()); {
} _sim.ProcessCommand(new ResumeSimulationCommand());
}
private void HandleEditEvents(IReadOnlyList<IWorldEvent> events)
{ _running = true;
foreach (var evt in events) _controlBar.UpdateForPhase(SimPhase.Running);
{ _simTimer.WaitTime = _simInterval;
switch (evt) _simTimer.Start();
{ }
case PiecePlacedEvent placed:
CreatePieceVisual(placed); private void OnPause()
UpdateStockFromSnapshot(); {
break; if (_sim == null) return;
_sim.ProcessCommand(new PauseSimulationCommand());
case PieceRemovedEvent removed: _running = false;
_eventAnimator.UnregisterPiece(removed.PieceId); _simTimer.Stop();
UpdateStockFromSnapshot(); _controlBar.UpdateForPhase(SimPhase.Paused);
_detailPanel.Hide(); }
break;
private void OnStep()
case PlacementRejectedEvent rejected: {
GD.Print($"Placement rejected: {rejected.Reason}"); if (_sim == null || _eventAnimator.IsAnimating) return;
break; var events = _sim.ProcessCommand(new StepSimulationCommand());
_eventAnimator.ProcessEvents(events);
case CommandRejectedEvent rejected: _controlBar.UpdateForPhase(_sim.GetSnapshot().Phase);
GD.Print($"Command rejected: {rejected.Reason}"); }
break;
} private void OnStop()
} {
} if (_sim == null) return;
_running = false;
private void CreatePieceVisual(PiecePlacedEvent placed) _simTimer.Stop();
{ _sim.ProcessCommand(new StopSimulationCommand());
var pieceView = new PieceView(); _eventAnimator.ResetPiecePositions(_sim.GetSnapshot());
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView); _controlBar.UpdateForPhase(SimPhase.Edit);
_boardView.AddChild(pieceView); _controlBar.ResetTurn();
_metricsOverlay.Hide();
var color = placed.Kind switch _inputMapper.SetSnapshot(_sim.GetSnapshot());
{
PieceKind.Rook => new Color("#4A7AB5"), // Reset objective panel
PieceKind.Bishop => new Color("#B54A8E"), if (_currentLevel != null)
PieceKind.Knight => new Color("#B5824A"), _objectivePanel.Setup(_currentLevel.Demands);
_ => Colors.White }
};
private void OnSpeedChanged(float interval)
var trajectView = new TrajectView(); {
trajectView.Setup(placed.PieceId, _simInterval = interval;
_boardView.CoordsToPixel(placed.Start), if (_simTimer.TimeLeft > 0)
_boardView.CoordsToPixel(placed.End), _simTimer.WaitTime = interval;
color); }
_boardView.AddChild(trajectView);
private void OnSimTimerTick()
_eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView); {
} if (_sim == null || _eventAnimator.IsAnimating) return;
private void UpdateStockFromSnapshot() var events = _sim.ProcessCommand(new StepSimulationCommand());
{ _eventAnimator.ProcessEvents(events);
if (_sim == null) return; }
var snap = _sim.GetSnapshot();
foreach (var (kind, remaining) in snap.RemainingStock) private void OnTurnAnimationCompleted()
_pieceStockPanel.UpdateCount(kind, remaining); {
} if (_sim == null) return;
var phase = _sim.GetSnapshot().Phase;
// --- Exec Phase --- _controlBar.UpdateForPhase(phase);
private void OnPlay() if (phase == SimPhase.Victory || phase == SimPhase.Defeat || phase == SimPhase.Collision)
{ {
if (_sim == null) return; _running = false;
_simTimer.Stop();
var snap = _sim.GetSnapshot(); }
if (snap.Phase == SimPhase.Edit) }
{
var events = _sim.ProcessCommand(new StartSimulationCommand()); private void OnVictory()
foreach (var evt in events) {
{ _running = false;
if (evt is CommandRejectedEvent r) _simTimer.Stop();
{ }
GD.Print($"Cannot start: {r.Reason}");
return; private void OnCollision()
} {
} _running = false;
} _simTimer.Stop();
else if (snap.Phase == SimPhase.Paused) _controlBar.UpdateForPhase(SimPhase.Collision);
{ }
_sim.ProcessCommand(new ResumeSimulationCommand());
} // --- Navigation ---
_running = true; private void OnRetry()
_controlBar.UpdateForPhase(SimPhase.Running); {
_simTimer.WaitTime = _simInterval; LoadLevel(_currentLevelIndex);
_simTimer.Start(); }
}
private void OnNextLevel()
private void OnPause() {
{ if (_currentLevelIndex + 1 < LevelFiles.Length)
if (_sim == null) return; LoadLevel(_currentLevelIndex + 1);
_sim.ProcessCommand(new PauseSimulationCommand()); else
_running = false; ShowLevelSelect();
_simTimer.Stop(); }
_controlBar.UpdateForPhase(SimPhase.Paused);
}
private void OnStep()
{
if (_sim == null || _eventAnimator.IsAnimating) return;
var events = _sim.ProcessCommand(new StepSimulationCommand());
_eventAnimator.ProcessEvents(events);
_controlBar.UpdateForPhase(_sim.GetSnapshot().Phase);
}
private void OnStop()
{
if (_sim == null) return;
_running = false;
_simTimer.Stop();
_sim.ProcessCommand(new StopSimulationCommand());
_eventAnimator.ResetPiecePositions(_sim.GetSnapshot());
_controlBar.UpdateForPhase(SimPhase.Edit);
_controlBar.ResetTurn();
_metricsOverlay.Hide();
_inputMapper.SetSnapshot(_sim.GetSnapshot());
// Reset objective panel
if (_currentLevel != null)
_objectivePanel.Setup(_currentLevel.Demands);
}
private void OnSpeedChanged(float interval)
{
_simInterval = interval;
if (_simTimer.TimeLeft > 0)
_simTimer.WaitTime = interval;
}
private void OnSimTimerTick()
{
if (_sim == null || _eventAnimator.IsAnimating) return;
var events = _sim.ProcessCommand(new StepSimulationCommand());
_eventAnimator.ProcessEvents(events);
}
private void OnTurnAnimationCompleted()
{
if (_sim == null) return;
var phase = _sim.GetSnapshot().Phase;
_controlBar.UpdateForPhase(phase);
if (phase == SimPhase.Victory || phase == SimPhase.Defeat)
{
_running = false;
_simTimer.Stop();
}
}
private void OnVictory()
{
_running = false;
_simTimer.Stop();
}
// --- Navigation ---
private void OnRetry()
{
LoadLevel(_currentLevelIndex);
}
private void OnNextLevel()
{
if (_currentLevelIndex + 1 < LevelFiles.Length)
LoadLevel(_currentLevelIndex + 1);
else
ShowLevelSelect();
}
} }

View file

@ -1 +0,0 @@
uid://dygonjc0xhp15

View file

@ -15,7 +15,6 @@ public partial class PieceView : Node2D
public Coords StartCell { get; private set; } public Coords StartCell { get; private set; }
public Coords EndCell { get; private set; } public Coords EndCell { get; private set; }
private static readonly Color PawnColor = new("#7AB54A");
private static readonly Color RookColor = new("#4A7AB5"); private static readonly Color RookColor = new("#4A7AB5");
private static readonly Color BishopColor = new("#B54A8E"); private static readonly Color BishopColor = new("#B54A8E");
private static readonly Color KnightColor = new("#B5824A"); private static readonly Color KnightColor = new("#B5824A");
@ -33,7 +32,6 @@ public partial class PieceView : Node2D
var color = kind switch var color = kind switch
{ {
PieceKind.Pawn => PawnColor,
PieceKind.Rook => RookColor, PieceKind.Rook => RookColor,
PieceKind.Bishop => BishopColor, PieceKind.Bishop => BishopColor,
PieceKind.Knight => KnightColor, PieceKind.Knight => KnightColor,
@ -59,7 +57,6 @@ public partial class PieceView : Node2D
{ {
Text = kind switch Text = kind switch
{ {
PieceKind.Pawn => "P",
PieceKind.Rook => "T", PieceKind.Rook => "T",
PieceKind.Bishop => "F", PieceKind.Bishop => "F",
PieceKind.Knight => "C", PieceKind.Knight => "C",
@ -67,8 +64,7 @@ public partial class PieceView : Node2D
}, },
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Position = new Vector2(-8, -10), Position = new Vector2(-8, -10)
MouseFilter = Control.MouseFilterEnum.Ignore
}; };
_label.AddThemeFontSizeOverride("font_size", 16); _label.AddThemeFontSizeOverride("font_size", 16);
_label.AddThemeColorOverride("font_color", Colors.White); _label.AddThemeColorOverride("font_color", Colors.White);
@ -79,8 +75,7 @@ public partial class PieceView : Node2D
{ {
Size = new Vector2(14, 14), Size = new Vector2(14, 14),
Position = new Vector2(-7, -30), Position = new Vector2(-7, -30),
Visible = false, Visible = false
MouseFilter = Control.MouseFilterEnum.Ignore
}; };
AddChild(_cargoIndicator); AddChild(_cargoIndicator);
} }

View file

@ -1 +0,0 @@
uid://cqnkhh3lwxrcg

View file

@ -1 +0,0 @@
uid://d4mdddae1m0ia

View file

@ -1,7 +1,6 @@
using Godot; using Godot;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Chessistics.Engine.Events; using Chessistics.Engine.Events;
using Chessistics.Engine.Model; using Chessistics.Engine.Model;
using Chessistics.Scripts.Board; using Chessistics.Scripts.Board;
@ -23,19 +22,12 @@ public partial class EventAnimator : Node
private bool _animating; private bool _animating;
public bool IsAnimating => _animating; public bool IsAnimating => _animating;
private static readonly Color WoodCargoColor = new("#8B6914");
private static readonly Color StoneCargoColor = new("#808080");
private const float ProduceDuration = 0.3f;
private const float TransferDuration = 0.25f;
private const float MoveDuration = 0.3f;
private const float KnightMoveDuration = 0.4f;
private const float DestroyDuration = 0.3f;
[Signal] [Signal]
public delegate void TurnAnimationCompletedEventHandler(); public delegate void TurnAnimationCompletedEventHandler();
[Signal] [Signal]
public delegate void VictoryReachedEventHandler(); public delegate void VictoryReachedEventHandler();
[Signal]
public delegate void CollisionOccurredEventHandler();
public void Initialize(BoardView boardView, ObjectivePanel objectivePanel, public void Initialize(BoardView boardView, ObjectivePanel objectivePanel,
ControlBar controlBar, MetricsOverlay metricsOverlay) ControlBar controlBar, MetricsOverlay metricsOverlay)
@ -72,39 +64,54 @@ public partial class EventAnimator : Node
var tween = CreateTween(); var tween = CreateTween();
tween.SetParallel(false); tween.SetParallel(false);
var produceEvents = new List<CargoProducedEvent>();
var transferEvents = new List<IWorldEvent>();
var moveEvents = new List<PieceMovedEvent>();
var collisionEvents = new List<PieceDestroyedEvent>();
foreach (var evt in events) foreach (var evt in events)
{ {
switch (evt) switch (evt)
{ {
case TurnStartedEvent ts: case TurnStartedEvent ts:
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
tween.TweenCallback(Callable.From(() => _controlBar.UpdateTurn(ts.TurnNumber))); tween.TweenCallback(Callable.From(() => _controlBar.UpdateTurn(ts.TurnNumber)));
break; break;
case CargoProducedEvent produced:
produceEvents.Add(produced);
break;
case CargoTransferredEvent:
case DemandProgressEvent:
transferEvents.Add(evt);
break;
case PieceMovedEvent moved: case PieceMovedEvent moved:
moveEvents.Add(moved); if (_pieceViews.TryGetValue(moved.PieceId, out var pv))
{
var target = _boardView.CoordsToPixel(moved.To);
var piece = pv;
var kind = piece.Kind;
float duration = kind == PieceKind.Knight ? 0.4f : 0.3f;
tween.TweenProperty(piece, "position", target, duration);
}
break; break;
case PieceDestroyedEvent destroyed: case CollisionDetectedEvent collision:
collisionEvents.Add(destroyed); tween.TweenCallback(Callable.From(() =>
{
FlashPiece(collision.PieceIdA);
FlashPiece(collision.PieceIdB);
EmitSignal(SignalName.CollisionOccurred);
}));
break;
case CargoTransferredEvent transfer:
tween.TweenCallback(Callable.From(() =>
{
if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value))
_pieceViews[transfer.GivingPieceId.Value].SetCargo(null);
if (transfer.ReceivingPieceId != null && _pieceViews.ContainsKey(transfer.ReceivingPieceId.Value))
_pieceViews[transfer.ReceivingPieceId.Value].SetCargo(transfer.Type);
}));
tween.TweenInterval(0.15f);
break;
case CargoProducedEvent:
break; // visual pulse could go here
case DemandProgressEvent progress:
tween.TweenCallback(Callable.From(() =>
_objectivePanel.UpdateProgress(progress.DemandCell, progress.Name, progress.Current, progress.Required)));
break; break;
case VictoryEvent victory: case VictoryEvent victory:
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
_metricsOverlay.ShowMetrics(victory.Metrics); _metricsOverlay.ShowMetrics(victory.Metrics);
@ -112,17 +119,16 @@ public partial class EventAnimator : Node
})); }));
break; break;
case TurnEndedEvent: case DeadlineExpiredEvent:
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents); tween.TweenCallback(Callable.From(() =>
EmitSignal(SignalName.CollisionOccurred))); // reuse for pause
break; break;
default: case TurnEndedEvent:
break; break;
} }
} }
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
tween.TweenCallback(Callable.From(() => tween.TweenCallback(Callable.From(() =>
{ {
_animating = false; _animating = false;
@ -130,141 +136,6 @@ public partial class EventAnimator : Node
})); }));
} }
private void FlushPhases(
Tween tween,
List<CargoProducedEvent> produceEvents,
List<IWorldEvent> transferEvents,
List<PieceMovedEvent> moveEvents,
List<PieceDestroyedEvent> collisionEvents)
{
// Phase 1: Produce — flash production cells
if (produceEvents.Count > 0)
{
tween.TweenCallback(Callable.From(() =>
{
foreach (var evt in produceEvents.ToList())
{
var cell = _boardView.GetCellView(evt.ProductionCell);
cell?.FlashProduce(ProduceDuration);
}
}));
tween.TweenInterval(ProduceDuration);
produceEvents.Clear();
}
// Phase 2: Transfers — animate cargo sliding from giver to receiver
if (transferEvents.Count > 0)
{
// Capture the events list before clearing
var eventsToAnimate = transferEvents.ToList();
// Step 1: remove cargo from givers + spawn sliding cargo sprites
tween.TweenCallback(Callable.From(() =>
{
foreach (var evt in eventsToAnimate)
{
if (evt is CargoTransferredEvent transfer)
{
// Remove cargo indicator from giver
if (transfer.GivingPieceId != null && _pieceViews.ContainsKey(transfer.GivingPieceId.Value))
_pieceViews[transfer.GivingPieceId.Value].SetCargo(null);
// Create sliding cargo sprite
SpawnCargoSlide(transfer);
}
}
}));
// Step 2: wait for slide, then show cargo on receivers + update demand progress
tween.TweenInterval(TransferDuration);
tween.TweenCallback(Callable.From(() =>
{
foreach (var evt in eventsToAnimate)
{
if (evt is CargoTransferredEvent transfer)
{
if (transfer.ReceivingPieceId != null && _pieceViews.ContainsKey(transfer.ReceivingPieceId.Value))
_pieceViews[transfer.ReceivingPieceId.Value].SetCargo(transfer.Type);
}
else if (evt is DemandProgressEvent progress)
{
_objectivePanel.UpdateProgress(progress.DemandCell, progress.Name, progress.Current, progress.Required);
}
}
}));
transferEvents.Clear();
}
// Phase 3: Movement — all pieces move simultaneously
if (moveEvents.Count > 0)
{
tween.SetParallel(true);
foreach (var moved in moveEvents)
{
if (_pieceViews.TryGetValue(moved.PieceId, out var pv))
{
var target = _boardView.CoordsToPixel(moved.To);
float duration = pv.Kind == PieceKind.Knight ? KnightMoveDuration : MoveDuration;
tween.TweenProperty(pv, "position", target, duration);
}
}
tween.SetParallel(false);
moveEvents.Clear();
}
// Phase 4: Collision/Destruction
if (collisionEvents.Count > 0)
{
tween.SetParallel(true);
foreach (var destroyed in collisionEvents)
{
var pieceId = destroyed.PieceId;
tween.TweenCallback(Callable.From(() =>
{
FlashPiece(pieceId);
UnregisterPiece(pieceId);
}));
}
tween.SetParallel(false);
tween.TweenInterval(DestroyDuration);
collisionEvents.Clear();
}
}
/// <summary>
/// Creates a temporary colored square that slides from the giver to the receiver.
/// </summary>
private void SpawnCargoSlide(CargoTransferredEvent transfer)
{
var from = _boardView.CoordsToPixel(transfer.From);
var to = _boardView.CoordsToPixel(transfer.To);
var color = transfer.Type switch
{
CargoType.Wood => WoodCargoColor,
CargoType.Stone => StoneCargoColor,
_ => Colors.White
};
var sprite = new ColorRect
{
Size = new Vector2(14, 14),
Position = new Vector2(-7, -7),
Color = color,
MouseFilter = Control.MouseFilterEnum.Ignore
};
var container = new Node2D { Position = from };
container.AddChild(sprite);
_boardView.AddChild(container);
var slideTween = container.CreateTween();
slideTween.TweenProperty(container, "position", to, TransferDuration)
.SetEase(Tween.EaseType.InOut)
.SetTrans(Tween.TransitionType.Cubic);
slideTween.TweenCallback(Callable.From(() => container.QueueFree()));
}
private void FlashPiece(int pieceId) private void FlashPiece(int pieceId)
{ {
if (!_pieceViews.TryGetValue(pieceId, out var pv)) return; if (!_pieceViews.TryGetValue(pieceId, out var pv)) return;

View file

@ -1 +0,0 @@
uid://cq8o11cyd42nr

View file

@ -1 +0,0 @@
uid://0lb2jejsmsyu

View file

@ -1 +0,0 @@
uid://b7pd55grwrmb7

View file

@ -12,210 +12,99 @@ public partial class LevelSelectScreen : Control
[ [
("Premier Convoi", "Acheminez du bois de la scierie au depot."), ("Premier Convoi", "Acheminez du bois de la scierie au depot."),
("Deux Clients", "Fournissez deux destinations depuis une seule scierie."), ("Deux Clients", "Fournissez deux destinations depuis une seule scierie."),
("Le Col", "Franchissez le mur et gerez deux types de cargaison."), ("Le Col", "Franchissez le mur et gerez deux types de cargaison.")
("Le Carrefour", "Deux productions, deux demandes, et un carrefour au centre."),
("Le Labyrinthe", "Un couloir etroit serpente a travers les murs."),
("Trois Royaumes", "Trois productions, trois demandes. Gerez un reseau complet.")
]; ];
public override void _Ready() public override void _Ready()
{ {
// Full-screen dark background var panel = new PanelContainer();
var bg = new PanelContainer(); panel.SetAnchorsPreset(LayoutPreset.FullRect);
bg.SetAnchorsPreset(LayoutPreset.FullRect);
var bgStyle = new StyleBoxFlat { BgColor = new Color(0.12f, 0.12f, 0.14f) };
bg.AddThemeStyleboxOverride("panel", bgStyle);
bg.MouseFilter = MouseFilterEnum.Ignore;
AddChild(bg);
// Outer margin
var margin = new MarginContainer(); var margin = new MarginContainer();
margin.SetAnchorsPreset(LayoutPreset.FullRect); margin.AddThemeConstantOverride("margin_left", 60);
margin.AddThemeConstantOverride("margin_left", 80); margin.AddThemeConstantOverride("margin_right", 60);
margin.AddThemeConstantOverride("margin_right", 80);
margin.AddThemeConstantOverride("margin_top", 60); margin.AddThemeConstantOverride("margin_top", 60);
margin.AddThemeConstantOverride("margin_bottom", 60); margin.AddThemeConstantOverride("margin_bottom", 60);
margin.MouseFilter = MouseFilterEnum.Ignore;
var outerVBox = new VBoxContainer(); var vbox = new VBoxContainer();
outerVBox.AddThemeConstantOverride("separation", 0);
outerVBox.MouseFilter = MouseFilterEnum.Ignore;
// --- Header section ---
var headerBox = new VBoxContainer();
headerBox.AddThemeConstantOverride("separation", 4);
headerBox.MouseFilter = MouseFilterEnum.Ignore;
var title = new Label var title = new Label
{ {
Text = "CHESSISTICS", Text = "CHESSISTICS",
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
title.AddThemeFontSizeOverride("font_size", 48); title.AddThemeFontSizeOverride("font_size", 32);
title.AddThemeColorOverride("font_color", new Color("#FFD700")); title.AddThemeColorOverride("font_color", new Color("#FFD700"));
headerBox.AddChild(title); vbox.AddChild(title);
var subtitle = new Label var subtitle = new Label
{ {
Text = "Selectionnez un niveau", Text = "Prototype — Selectionnez un niveau",
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
subtitle.AddThemeFontSizeOverride("font_size", 15); subtitle.AddThemeFontSizeOverride("font_size", 14);
subtitle.AddThemeColorOverride("font_color", new Color("#777777")); subtitle.AddThemeColorOverride("font_color", new Color("#AAAAAA"));
headerBox.AddChild(subtitle); vbox.AddChild(subtitle);
outerVBox.AddChild(headerBox); vbox.AddChild(new HSeparator());
// Spacer var grid = new HBoxContainer();
outerVBox.AddChild(new Control { CustomMinimumSize = new Vector2(0, 48) }); grid.Alignment = BoxContainer.AlignmentMode.Center;
// --- Level cards in a scrollable grid ---
var scroll = new ScrollContainer
{
SizeFlagsVertical = SizeFlags.ExpandFill,
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled
};
var grid = new GridContainer
{
Columns = 3,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
MouseFilter = MouseFilterEnum.Ignore
};
grid.AddThemeConstantOverride("h_separation", 28);
grid.AddThemeConstantOverride("v_separation", 28);
for (int i = 0; i < _levels.Length; i++) for (int i = 0; i < _levels.Length; i++)
{ {
var (name, desc) = _levels[i]; var (name, desc) = _levels[i];
grid.AddChild(CreateLevelCard(i, name, desc)); var card = CreateLevelCard(i, name, desc);
grid.AddChild(card);
} }
scroll.AddChild(grid); vbox.AddChild(grid);
outerVBox.AddChild(scroll); margin.AddChild(vbox);
panel.AddChild(margin);
margin.AddChild(outerVBox); AddChild(panel);
AddChild(margin);
} }
private Control CreateLevelCard(int index, string name, string description) private Control CreateLevelCard(int index, string name, string description)
{ {
var card = new PanelContainer var card = new PanelContainer
{ {
CustomMinimumSize = new Vector2(300, 240), CustomMinimumSize = new Vector2(220, 160)
SizeFlagsVertical = SizeFlags.ShrinkCenter
}; };
var cardStyle = new StyleBoxFlat
{
BgColor = new Color(0.17f, 0.17f, 0.19f),
BorderColor = new Color(0.28f, 0.28f, 0.32f),
BorderWidthBottom = 1,
BorderWidthTop = 1,
BorderWidthLeft = 1,
BorderWidthRight = 1,
CornerRadiusTopLeft = 8,
CornerRadiusTopRight = 8,
CornerRadiusBottomLeft = 8,
CornerRadiusBottomRight = 8,
ContentMarginLeft = 24,
ContentMarginRight = 24,
ContentMarginTop = 24,
ContentMarginBottom = 24
};
card.AddThemeStyleboxOverride("panel", cardStyle);
var vbox = new VBoxContainer(); var vbox = new VBoxContainer();
vbox.AddThemeConstantOverride("separation", 10);
// Level number
var numLabel = new Label var numLabel = new Label
{ {
Text = $"Niveau {index + 1}", Text = $"Niveau {index + 1}",
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
numLabel.AddThemeFontSizeOverride("font_size", 12); numLabel.AddThemeFontSizeOverride("font_size", 12);
numLabel.AddThemeColorOverride("font_color", new Color("#666666")); numLabel.AddThemeColorOverride("font_color", new Color("#AAAAAA"));
vbox.AddChild(numLabel); vbox.AddChild(numLabel);
// Level name
var nameLabel = new Label var nameLabel = new Label
{ {
Text = name, Text = name,
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
nameLabel.AddThemeFontSizeOverride("font_size", 22); nameLabel.AddThemeFontSizeOverride("font_size", 18);
nameLabel.AddThemeColorOverride("font_color", new Color("#EEEEEE"));
vbox.AddChild(nameLabel); vbox.AddChild(nameLabel);
// Thin separator
var sep = new HSeparator { CustomMinimumSize = new Vector2(0, 4) };
vbox.AddChild(sep);
// Description
var descLabel = new Label var descLabel = new Label
{ {
Text = description, Text = description,
HorizontalAlignment = HorizontalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center,
AutowrapMode = TextServer.AutowrapMode.Word, AutowrapMode = TextServer.AutowrapMode.Word
CustomMinimumSize = new Vector2(240, 0)
}; };
descLabel.AddThemeFontSizeOverride("font_size", 13); descLabel.AddThemeFontSizeOverride("font_size", 11);
descLabel.AddThemeColorOverride("font_color", new Color("#999999")); descLabel.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
vbox.AddChild(descLabel); vbox.AddChild(descLabel);
// Flexible spacer
vbox.AddChild(new Control { SizeFlagsVertical = SizeFlags.ExpandFill });
// Play button
var playBtn = new Button var playBtn = new Button
{ {
Text = "Jouer", Text = "Jouer",
CustomMinimumSize = new Vector2(120, 38), CustomMinimumSize = new Vector2(100, 32)
SizeFlagsHorizontal = SizeFlags.ShrinkCenter
}; };
var btnNormal = new StyleBoxFlat
{
BgColor = new Color("#8B6914"),
CornerRadiusTopLeft = 6,
CornerRadiusTopRight = 6,
CornerRadiusBottomLeft = 6,
CornerRadiusBottomRight = 6,
ContentMarginLeft = 24,
ContentMarginRight = 24,
ContentMarginTop = 8,
ContentMarginBottom = 8
};
var btnHover = new StyleBoxFlat
{
BgColor = new Color("#B8860B"),
CornerRadiusTopLeft = 6,
CornerRadiusTopRight = 6,
CornerRadiusBottomLeft = 6,
CornerRadiusBottomRight = 6,
ContentMarginLeft = 24,
ContentMarginRight = 24,
ContentMarginTop = 8,
ContentMarginBottom = 8
};
var btnPressed = new StyleBoxFlat
{
BgColor = new Color("#6B5010"),
CornerRadiusTopLeft = 6,
CornerRadiusTopRight = 6,
CornerRadiusBottomLeft = 6,
CornerRadiusBottomRight = 6,
ContentMarginLeft = 24,
ContentMarginRight = 24,
ContentMarginTop = 8,
ContentMarginBottom = 8
};
playBtn.AddThemeStyleboxOverride("normal", btnNormal);
playBtn.AddThemeStyleboxOverride("hover", btnHover);
playBtn.AddThemeStyleboxOverride("pressed", btnPressed);
playBtn.AddThemeFontSizeOverride("font_size", 15);
var idx = index; var idx = index;
playBtn.Pressed += () => EmitSignal(SignalName.LevelSelected, idx); playBtn.Pressed += () => EmitSignal(SignalName.LevelSelected, idx);
vbox.AddChild(playBtn); vbox.AddChild(playBtn);

View file

@ -1 +0,0 @@
uid://dhg770rwx2gop

View file

@ -1 +0,0 @@
uid://dpi1a85m4gva8

View file

@ -16,7 +16,6 @@ public partial class ObjectivePanel : VBoxContainer
var title = new Label { Text = "OBJECTIFS" }; var title = new Label { Text = "OBJECTIFS" };
title.AddThemeFontSizeOverride("font_size", 16); title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());

View file

@ -1 +0,0 @@
uid://c5q1qrjsbrp6m

View file

@ -24,7 +24,6 @@ public partial class PieceStockPanel : VBoxContainer
var title = new Label { Text = "PIECES" }; var title = new Label { Text = "PIECES" };
title.AddThemeFontSizeOverride("font_size", 16); title.AddThemeFontSizeOverride("font_size", 16);
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
AddChild(title); AddChild(title);
AddChild(new HSeparator()); AddChild(new HSeparator());
@ -93,7 +92,6 @@ public partial class PieceStockPanel : VBoxContainer
private static string GetPieceName(PieceKind kind) => kind switch private static string GetPieceName(PieceKind kind) => kind switch
{ {
PieceKind.Pawn => "Pion",
PieceKind.Rook => "Tour II", PieceKind.Rook => "Tour II",
PieceKind.Bishop => "Fou II", PieceKind.Bishop => "Fou II",
PieceKind.Knight => "Cavalier", PieceKind.Knight => "Cavalier",

View file

@ -1 +0,0 @@
uid://b7vf8ury0hdts

View file

@ -1 +0,0 @@
uid://ci3ilv3dv7wxh

View file

@ -1 +0,0 @@
uid://qb3ybyegoh7k

View file

@ -1 +0,0 @@
uid://dhrp4e6pkpkym

View file

@ -10,14 +10,12 @@ public class PlacePieceCommand : WorldCommand
public PieceKind Kind { get; } public PieceKind Kind { get; }
public Coords Start { get; } public Coords Start { get; }
public Coords End { get; } public Coords End { get; }
public int Level { get; }
public PlacePieceCommand(PieceKind kind, Coords start, Coords end, int level = 1) public PlacePieceCommand(PieceKind kind, Coords start, Coords end)
{ {
Kind = kind; Kind = kind;
Start = start; Start = start;
End = end; End = end;
Level = level;
} }
public override void AssertApplicationConditions(BoardState state) public override void AssertApplicationConditions(BoardState state)
@ -46,7 +44,7 @@ public class PlacePieceCommand : WorldCommand
protected override void DoApply(BoardState state, List<IWorldEvent> changeList) protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
{ {
var piece = new PieceState( var piece = new PieceState(
state.NextPieceId++, Kind, Start, End, state.Pieces.Count, Level); state.NextPieceId++, Kind, Start, End, state.Pieces.Count);
piece.CargoFilter = InferCargoFilter(state, piece); piece.CargoFilter = InferCargoFilter(state, piece);
@ -191,7 +189,7 @@ public class StepSimulationCommand : WorldCommand
TurnExecutor.ExecuteTurn(state, changeList); TurnExecutor.ExecuteTurn(state, changeList);
// After a step, remain in Paused unless victory/defeat occurred // After a step, remain in Paused unless victory/defeat/collision occurred
if (state.Phase == SimPhase.Running) if (state.Phase == SimPhase.Running)
state.Phase = SimPhase.Paused; state.Phase = SimPhase.Paused;
} }
@ -208,10 +206,6 @@ public class StopSimulationCommand : WorldCommand
protected override void DoApply(BoardState state, List<IWorldEvent> changeList) protected override void DoApply(BoardState state, List<IWorldEvent> changeList)
{ {
// Restore destroyed pieces
state.Pieces.AddRange(state.DestroyedPieces);
state.DestroyedPieces.Clear();
foreach (var piece in state.Pieces) foreach (var piece in state.Pieces)
{ {
piece.CurrentCell = piece.StartCell; piece.CurrentCell = piece.StartCell;
@ -219,7 +213,7 @@ public class StopSimulationCommand : WorldCommand
} }
foreach (var pos in state.ProductionBuffers.Keys.ToList()) foreach (var pos in state.ProductionBuffers.Keys.ToList())
state.ProductionBuffers[pos] = 0; state.ProductionBuffers[pos] = null;
foreach (var demand in state.Demands.Values) foreach (var demand in state.Demands.Values)
demand.ReceivedCount = 0; demand.ReceivedCount = 0;

View file

@ -1 +0,0 @@
uid://blxykw7srkq2x

View file

@ -1 +0,0 @@
uid://cguse2y3ma1on

View file

@ -15,13 +15,13 @@ public record SimulationResumedEvent : IWorldEvent;
public record SimulationStoppedEvent : IWorldEvent; public record SimulationStoppedEvent : IWorldEvent;
public record LevelResetEvent : IWorldEvent; public record LevelResetEvent : IWorldEvent;
// Turn events — all carry TurnNumber for animation grouping // Turn events
public record TurnStartedEvent(int TurnNumber) : IWorldEvent; public record TurnStartedEvent(int TurnNumber) : IWorldEvent;
public record PieceMovedEvent(int TurnNumber, int PieceId, Coords From, Coords To) : IWorldEvent; public record PieceMovedEvent(int PieceId, Coords From, Coords To) : IWorldEvent;
public record PieceDestroyedEvent(int TurnNumber, int PieceId, int? DestroyerPieceId, Coords Cell) : IWorldEvent; public record CollisionDetectedEvent(int PieceIdA, int PieceIdB, Coords Cell) : IWorldEvent;
public record CargoTransferredEvent(int TurnNumber, Coords From, Coords To, CargoType Type, int? GivingPieceId, int? ReceivingPieceId) : IWorldEvent; public record CargoTransferredEvent(Coords From, Coords To, CargoType Type, int? GivingPieceId, int? ReceivingPieceId) : IWorldEvent;
public record CargoProducedEvent(int TurnNumber, Coords ProductionCell, CargoType Type) : IWorldEvent; public record CargoProducedEvent(Coords ProductionCell, CargoType Type) : IWorldEvent;
public record DemandProgressEvent(int TurnNumber, Coords DemandCell, string Name, int Current, int Required) : IWorldEvent; public record DemandProgressEvent(Coords DemandCell, string Name, int Current, int Required) : IWorldEvent;
public record VictoryEvent(int TurnNumber, Metrics Metrics) : IWorldEvent; public record VictoryEvent(Metrics Metrics) : IWorldEvent;
public record DeadlineExpiredEvent(int TurnNumber, Coords DemandCell, string Name) : IWorldEvent; public record DeadlineExpiredEvent(Coords DemandCell, string Name) : IWorldEvent;
public record TurnEndedEvent(int TurnNumber) : IWorldEvent; public record TurnEndedEvent(int TurnNumber) : IWorldEvent;

View file

@ -1 +0,0 @@
uid://cq5huqxbjpw2a

View file

@ -27,13 +27,13 @@ public static class LevelLoader
Width = dto.Width, Width = dto.Width,
Height = dto.Height, Height = dto.Height,
Productions = dto.Productions.Select(p => new ProductionDef( Productions = dto.Productions.Select(p => new ProductionDef(
new Coords(p.Col, p.Row), p.Name, ParseCargo(p.Cargo), p.Amount new Coords(p.Col, p.Row), p.Name, ParseCargo(p.Cargo), p.Interval
)).ToList(), )).ToList(),
Demands = dto.Demands.Select(d => new DemandDef( Demands = dto.Demands.Select(d => new DemandDef(
new Coords(d.Col, d.Row), d.Name, ParseCargo(d.Cargo), d.Amount, d.Deadline new Coords(d.Col, d.Row), d.Name, ParseCargo(d.Cargo), d.Amount, d.Deadline
)).ToList(), )).ToList(),
Walls = dto.Walls?.Select(w => new Coords(w.Col, w.Row)).ToList() ?? [], Walls = dto.Walls?.Select(w => new Coords(w.Col, w.Row)).ToList() ?? [],
Stock = dto.Stock.Select(s => new PieceStock(ParseKind(s.Kind), s.Count, s.Level)).ToList() Stock = dto.Stock.Select(s => new PieceStock(ParseKind(s.Kind), s.Count)).ToList()
}; };
} }
@ -52,7 +52,6 @@ public static class LevelLoader
private static PieceKind ParseKind(string kind) => kind.ToLowerInvariant() switch private static PieceKind ParseKind(string kind) => kind.ToLowerInvariant() switch
{ {
"pawn" => PieceKind.Pawn,
"rook" => PieceKind.Rook, "rook" => PieceKind.Rook,
"bishop" => PieceKind.Bishop, "bishop" => PieceKind.Bishop,
"knight" => PieceKind.Knight, "knight" => PieceKind.Knight,
@ -93,7 +92,7 @@ public static class LevelLoader
public int Row { get; set; } public int Row { get; set; }
public string Name { get; set; } = ""; public string Name { get; set; } = "";
public string Cargo { get; set; } = ""; public string Cargo { get; set; } = "";
public int Amount { get; set; } = 1; public int Interval { get; set; }
} }
private class DemandDto private class DemandDto
@ -116,6 +115,5 @@ public static class LevelLoader
{ {
public string Kind { get; set; } = ""; public string Kind { get; set; } = "";
public int Count { get; set; } public int Count { get; set; }
public int Level { get; set; } = 1;
} }
} }

View file

@ -1 +0,0 @@
uid://bbajsqcri0wa1

View file

@ -24,7 +24,7 @@ public class BoardSnapshot
Array.Copy(state.Grid, Grid, state.Grid.Length); Array.Copy(state.Grid, Grid, state.Grid.Length);
Productions = state.Productions.Values Productions = state.Productions.Values
.Select(p => new ProductionSnapshot(p.Position, p.Name, p.Cargo, p.Amount, state.ProductionBuffers[p.Position])) .Select(p => new ProductionSnapshot(p.Position, p.Name, p.Cargo, p.Interval, state.ProductionBuffers[p.Position]))
.ToList(); .ToList();
Demands = state.Demands.Values Demands = state.Demands.Values
@ -32,13 +32,13 @@ public class BoardSnapshot
.ToList(); .ToList();
Pieces = state.Pieces Pieces = state.Pieces
.Select(p => new PieceSnapshot(p.Id, p.Kind, p.Level, p.StartCell, p.EndCell, p.CurrentCell, p.Cargo, p.CargoFilter, p.SocialStatus)) .Select(p => new PieceSnapshot(p.Id, p.Kind, p.StartCell, p.EndCell, p.CurrentCell, p.Cargo, p.CargoFilter, p.SocialStatus))
.ToList(); .ToList();
RemainingStock = new Dictionary<PieceKind, int>(state.RemainingStock); RemainingStock = new Dictionary<PieceKind, int>(state.RemainingStock);
} }
} }
public record ProductionSnapshot(Coords Position, string Name, CargoType Cargo, int Amount, int BufferCount); public record ProductionSnapshot(Coords Position, string Name, CargoType Cargo, int Interval, CargoType? Buffer);
public record DemandSnapshot(Coords Position, string Name, CargoType Cargo, int Required, int Deadline, int ReceivedCount, bool IsSatisfied); public record DemandSnapshot(Coords Position, string Name, CargoType Cargo, int Required, int Deadline, int ReceivedCount, bool IsSatisfied);
public record PieceSnapshot(int Id, PieceKind Kind, int Level, Coords StartCell, Coords EndCell, Coords CurrentCell, CargoType? Cargo, CargoType? CargoFilter, int SocialStatus); public record PieceSnapshot(int Id, PieceKind Kind, Coords StartCell, Coords EndCell, Coords CurrentCell, CargoType? Cargo, CargoType? CargoFilter, int SocialStatus);

View file

@ -1 +0,0 @@
uid://cnpyeaslw4mnb

View file

@ -10,8 +10,7 @@ public class BoardState
public Dictionary<Coords, ProductionDef> Productions { get; } public Dictionary<Coords, ProductionDef> Productions { get; }
public Dictionary<Coords, DemandState> Demands { get; } public Dictionary<Coords, DemandState> Demands { get; }
public List<PieceState> Pieces { get; } public List<PieceState> Pieces { get; }
public List<PieceState> DestroyedPieces { get; } = new(); public Dictionary<Coords, CargoType?> ProductionBuffers { get; }
public Dictionary<Coords, int> ProductionBuffers { get; }
public SimPhase Phase { get; set; } public SimPhase Phase { get; set; }
public int TurnNumber { get; set; } public int TurnNumber { get; set; }
public int NextPieceId { get; set; } public int NextPieceId { get; set; }
@ -35,7 +34,7 @@ public class BoardState
Productions = new Dictionary<Coords, ProductionDef>(); Productions = new Dictionary<Coords, ProductionDef>();
Demands = new Dictionary<Coords, DemandState>(); Demands = new Dictionary<Coords, DemandState>();
Pieces = new List<PieceState>(); Pieces = new List<PieceState>();
ProductionBuffers = new Dictionary<Coords, int>(); ProductionBuffers = new Dictionary<Coords, CargoType?>();
RemainingStock = new Dictionary<PieceKind, int>(); RemainingStock = new Dictionary<PieceKind, int>();
OccupiedCells = new HashSet<Coords>(); OccupiedCells = new HashSet<Coords>();
@ -57,7 +56,7 @@ public class BoardState
{ {
Grid[prod.Position.Col, prod.Position.Row] = CellType.Production; Grid[prod.Position.Col, prod.Position.Row] = CellType.Production;
Productions[prod.Position] = prod; Productions[prod.Position] = prod;
ProductionBuffers[prod.Position] = 0; ProductionBuffers[prod.Position] = null;
} }
// Place demands // Place demands
@ -120,7 +119,6 @@ public class BoardState
public void ResetFromLevel() public void ResetFromLevel()
{ {
Pieces.Clear(); Pieces.Clear();
DestroyedPieces.Clear();
Productions.Clear(); Productions.Clear();
Demands.Clear(); Demands.Clear();
ProductionBuffers.Clear(); ProductionBuffers.Clear();
@ -142,7 +140,7 @@ public class BoardState
{ {
Grid[prod.Position.Col, prod.Position.Row] = CellType.Production; Grid[prod.Position.Col, prod.Position.Row] = CellType.Production;
Productions[prod.Position] = prod; Productions[prod.Position] = prod;
ProductionBuffers[prod.Position] = 0; ProductionBuffers[prod.Position] = null;
} }
foreach (var demand in _levelDef.Demands) foreach (var demand in _levelDef.Demands)

View file

@ -1 +0,0 @@
uid://dq6vosmhy6ofr

View file

@ -1 +0,0 @@
uid://dmuxaq4gvi3v1

View file

@ -1 +0,0 @@
uid://dblei0vt3ulge

View file

@ -1 +0,0 @@
uid://cr4rqwl666m6i

View file

@ -1 +0,0 @@
uid://iv2oeeyht17f

View file

@ -1 +0,0 @@
uid://dwakj4uour8j0

View file

@ -1 +0,0 @@
uid://dxfk6kjtkl5th

View file

@ -1 +0,0 @@
uid://boa1klorcvwn7

View file

@ -2,7 +2,6 @@ namespace Chessistics.Engine.Model;
public enum PieceKind public enum PieceKind
{ {
Pawn,
Rook, Rook,
Bishop, Bishop,
Knight Knight

View file

@ -1 +0,0 @@
uid://b7ayb8mmmmvos

View file

@ -4,7 +4,6 @@ public static class PieceRules
{ {
public static int GetSocialStatus(PieceKind kind) => kind switch public static int GetSocialStatus(PieceKind kind) => kind switch
{ {
PieceKind.Pawn => 1,
PieceKind.Rook => 5, PieceKind.Rook => 5,
PieceKind.Bishop => 3, PieceKind.Bishop => 3,
PieceKind.Knight => 3, PieceKind.Knight => 3,
@ -13,7 +12,6 @@ public static class PieceRules
public static int GetMaxRange(PieceKind kind) => kind switch public static int GetMaxRange(PieceKind kind) => kind switch
{ {
PieceKind.Pawn => 1,
PieceKind.Rook => 2, PieceKind.Rook => 2,
PieceKind.Bishop => 2, PieceKind.Bishop => 2,
PieceKind.Knight => 0, // Knight uses L-shape, not range PieceKind.Knight => 0, // Knight uses L-shape, not range

View file

@ -1 +0,0 @@
uid://g4se3fjdvw40

View file

@ -4,7 +4,6 @@ public class PieceState
{ {
public int Id { get; } public int Id { get; }
public PieceKind Kind { get; } public PieceKind Kind { get; }
public int Level { get; }
public Coords StartCell { get; } public Coords StartCell { get; }
public Coords EndCell { get; } public Coords EndCell { get; }
public Coords CurrentCell { get; set; } public Coords CurrentCell { get; set; }
@ -13,11 +12,10 @@ public class PieceState
public int SocialStatus { get; } public int SocialStatus { get; }
public int PlacementOrder { get; } public int PlacementOrder { get; }
public PieceState(int id, PieceKind kind, Coords startCell, Coords endCell, int placementOrder, int level = 1) public PieceState(int id, PieceKind kind, Coords startCell, Coords endCell, int placementOrder)
{ {
Id = id; Id = id;
Kind = kind; Kind = kind;
Level = level;
StartCell = startCell; StartCell = startCell;
EndCell = endCell; EndCell = endCell;
CurrentCell = startCell; CurrentCell = startCell;

View file

@ -1 +0,0 @@
uid://cqcbtrhwqx1oq

View file

@ -1,3 +1,3 @@
namespace Chessistics.Engine.Model; namespace Chessistics.Engine.Model;
public record PieceStock(PieceKind Kind, int Count, int Level = 1); public record PieceStock(PieceKind Kind, int Count);

View file

@ -1 +0,0 @@
uid://iyhkgjgadvbq

View file

@ -1,3 +1,3 @@
namespace Chessistics.Engine.Model; namespace Chessistics.Engine.Model;
public record ProductionDef(Coords Position, string Name, CargoType Cargo, int Amount = 1); public record ProductionDef(Coords Position, string Name, CargoType Cargo, int Interval);

View file

@ -1 +0,0 @@
uid://3jsqpr5wfblc

View file

@ -5,6 +5,7 @@ public enum SimPhase
Edit, Edit,
Running, Running,
Paused, Paused,
Collision,
Victory, Victory,
Defeat Defeat
} }

View file

@ -1 +0,0 @@
uid://cmspgsp8mcvtd

View file

@ -0,0 +1,34 @@
using Chessistics.Engine.Model;
namespace Chessistics.Engine.Rules;
public static class CollisionDetector
{
public static IReadOnlyList<(int PieceIdA, int PieceIdB, Coords Cell)> DetectCollisions(
IReadOnlyList<PieceState> pieces)
{
var collisions = new List<(int, int, Coords)>();
var byCell = new Dictionary<Coords, List<PieceState>>();
foreach (var piece in pieces)
{
if (!byCell.TryGetValue(piece.CurrentCell, out var list))
{
list = [];
byCell[piece.CurrentCell] = list;
}
list.Add(piece);
}
foreach (var (cell, occupants) in byCell)
{
if (occupants.Count < 2) continue;
for (int i = 0; i < occupants.Count; i++)
for (int j = i + 1; j < occupants.Count; j++)
collisions.Add((occupants[i].Id, occupants[j].Id, cell));
}
return collisions;
}
}

View file

@ -1,55 +0,0 @@
using Chessistics.Engine.Model;
namespace Chessistics.Engine.Rules;
public static class CollisionResolver
{
/// <summary>
/// Resolves collisions after movement. For each cell with 2+ pieces,
/// the strongest piece survives and destroys the others.
/// Priority: SocialStatus desc → Level desc → mutual destruction on exact tie.
/// </summary>
public static List<(PieceState? Survivor, List<PieceState> Destroyed, Coords Cell)> ResolveCollisions(
IReadOnlyList<PieceState> pieces)
{
var results = new List<(PieceState? Survivor, List<PieceState> Destroyed, Coords Cell)>();
var byCell = new Dictionary<Coords, List<PieceState>>();
foreach (var piece in pieces)
{
if (!byCell.TryGetValue(piece.CurrentCell, out var list))
{
list = [];
byCell[piece.CurrentCell] = list;
}
list.Add(piece);
}
foreach (var (cell, occupants) in byCell)
{
if (occupants.Count < 2) continue;
// Sort by priority: highest status first, then highest level
var sorted = occupants
.OrderByDescending(p => p.SocialStatus)
.ThenByDescending(p => p.Level)
.ToList();
var top = sorted[0];
var second = sorted[1];
// If top two have same status AND same level → mutual destruction
if (top.SocialStatus == second.SocialStatus && top.Level == second.Level)
{
results.Add((null, sorted, cell));
}
else
{
var destroyed = sorted.Skip(1).ToList();
results.Add((top, destroyed, cell));
}
}
return results;
}
}

View file

@ -1 +0,0 @@
uid://ddl7yl8k7h4qp

View file

@ -19,7 +19,6 @@ public static class MoveValidator
return kind switch return kind switch
{ {
PieceKind.Pawn => GetSlidingMoves(start, OrthogonalDirs, 1, board),
PieceKind.Rook => GetSlidingMoves(start, OrthogonalDirs, 2, board), PieceKind.Rook => GetSlidingMoves(start, OrthogonalDirs, 2, board),
PieceKind.Bishop => GetSlidingMoves(start, DiagonalDirs, 2, board), PieceKind.Bishop => GetSlidingMoves(start, DiagonalDirs, 2, board),
PieceKind.Knight => GetKnightMoves(start, board), PieceKind.Knight => GetKnightMoves(start, board),

View file

@ -1 +0,0 @@
uid://danfmpxdyyc3w

View file

@ -26,33 +26,29 @@ public static class TransferResolver
{ {
// Sort productions deterministically (by position) // Sort productions deterministically (by position)
var productions = state.Productions.Values var productions = state.Productions.Values
.Where(p => state.ProductionBuffers[p.Position] > 0) .Where(p => state.ProductionBuffers[p.Position] != null)
.OrderBy(p => p.Position.Col).ThenBy(p => p.Position.Row) .OrderBy(p => p.Position.Col).ThenBy(p => p.Position.Row)
.ToList(); .ToList();
foreach (var prod in productions) foreach (var prod in productions)
{ {
var cargoType = prod.Cargo; var cargoType = state.ProductionBuffers[prod.Position]!.Value;
// Find adjacent pieces without cargo that accept this cargo type // Find adjacent pieces without cargo that accept this cargo type
var receivers = GetAdjacentPiecesWithoutCargo(state, prod.Position, participated, var receivers = GetAdjacentPiecesWithoutCargo(state, prod.Position, participated,
cargoType: cargoType); cargoType: cargoType);
foreach (var receiver in receivers) if (receivers.Count == 0) continue;
{
if (state.ProductionBuffers[prod.Position] <= 0) break;
receiver.Cargo = cargoType; var receiver = receivers[0];
state.ProductionBuffers[prod.Position]--; receiver.Cargo = cargoType;
participated.Add(receiver.Id); state.ProductionBuffers[prod.Position] = null;
participated.Add(receiver.Id);
productionGave.Add(prod.Position);
events.Add(new CargoTransferredEvent( events.Add(new CargoTransferredEvent(
state.TurnNumber, prod.Position, receiver.CurrentCell, cargoType, prod.Position, receiver.CurrentCell, cargoType,
GivingPieceId: null, ReceivingPieceId: receiver.Id)); GivingPieceId: null, ReceivingPieceId: receiver.Id));
}
if (state.ProductionBuffers[prod.Position] < prod.Amount)
productionGave.Add(prod.Position);
} }
} }
@ -63,7 +59,8 @@ public static class TransferResolver
var givers = state.Pieces var givers = state.Pieces
.Where(p => p.Cargo != null && !participated.Contains(p.Id)) .Where(p => p.Cargo != null && !participated.Contains(p.Id))
.OrderByDescending(p => p.SocialStatus) .OrderByDescending(p => p.SocialStatus)
.ThenByDescending(p => p.Level) .ThenBy(p => MinDistanceToProduction(p.CurrentCell, state, p.Cargo))
.ThenBy(p => p.PlacementOrder)
.ToList(); .ToList();
foreach (var giver in givers) foreach (var giver in givers)
@ -72,7 +69,7 @@ public static class TransferResolver
var cargoType = giver.Cargo!.Value; var cargoType = giver.Cargo!.Value;
// Priority 1: deliver to adjacent demand (always accepts matching cargo, even when satisfied) // Priority 1: deliver to adjacent demand
var adjacentDemand = GetAdjacentCompatibleDemand(state, giver.CurrentCell, cargoType); var adjacentDemand = GetAdjacentCompatibleDemand(state, giver.CurrentCell, cargoType);
if (adjacentDemand != null) if (adjacentDemand != null)
{ {
@ -81,19 +78,20 @@ public static class TransferResolver
participated.Add(giver.Id); participated.Add(giver.Id);
events.Add(new CargoTransferredEvent( events.Add(new CargoTransferredEvent(
state.TurnNumber, giver.CurrentCell, adjacentDemand.Position, cargoType, giver.CurrentCell, adjacentDemand.Position, cargoType,
GivingPieceId: giver.Id, ReceivingPieceId: null)); GivingPieceId: giver.Id, ReceivingPieceId: null));
events.Add(new DemandProgressEvent( events.Add(new DemandProgressEvent(
state.TurnNumber, adjacentDemand.Position, adjacentDemand.Name, adjacentDemand.Position, adjacentDemand.Name,
adjacentDemand.ReceivedCount, adjacentDemand.Required)); adjacentDemand.ReceivedCount, adjacentDemand.Required));
continue; continue;
} }
// Priority 2: transfer to adjacent piece without cargo // Priority 2: transfer to adjacent piece without cargo
// Prefer receivers farther from production (push cargo forward in chain)
var receivers = GetAdjacentPiecesWithoutCargo(state, giver.CurrentCell, participated, var receivers = GetAdjacentPiecesWithoutCargo(state, giver.CurrentCell, participated,
cargoType: cargoType); forwardDirection: true, cargoType: cargoType);
if (receivers.Count == 0) continue; if (receivers.Count == 0) continue;
var receiver = receivers[0]; var receiver = receivers[0];
@ -103,26 +101,32 @@ public static class TransferResolver
participated.Add(receiver.Id); participated.Add(receiver.Id);
events.Add(new CargoTransferredEvent( events.Add(new CargoTransferredEvent(
state.TurnNumber, giver.CurrentCell, receiver.CurrentCell, cargoType, giver.CurrentCell, receiver.CurrentCell, cargoType,
GivingPieceId: giver.Id, ReceivingPieceId: receiver.Id)); GivingPieceId: giver.Id, ReceivingPieceId: receiver.Id));
} }
} }
private static List<PieceState> GetAdjacentPiecesWithoutCargo( private static List<PieceState> GetAdjacentPiecesWithoutCargo(
BoardState state, Coords position, HashSet<int> participated, BoardState state, Coords position, HashSet<int> participated,
CargoType? cargoType = null) bool forwardDirection = false, CargoType? cargoType = null)
{ {
var adjacent = position.GetAdjacent4(state.Width, state.Height); var adjacent = position.GetAdjacent4(state.Width, state.Height);
return state.Pieces var query = state.Pieces
.Where(p => p.Cargo == null .Where(p => p.Cargo == null
&& !participated.Contains(p.Id) && !participated.Contains(p.Id)
&& adjacent.Contains(p.CurrentCell) && adjacent.Contains(p.CurrentCell)
&& (p.CargoFilter == null || cargoType == null || p.CargoFilter == cargoType)) && (p.CargoFilter == null || cargoType == null || p.CargoFilter == cargoType))
.OrderByDescending(p => p.SocialStatus) .OrderByDescending(p => p.SocialStatus);
.ThenByDescending(p => p.Level)
.ThenBy(p => ClockwiseOrder(p.CurrentCell, position, state.TurnNumber)) // For piece-to-piece transfers, prefer receivers farther from production
.ToList(); // (pushes cargo forward through relay chains instead of backward).
// For production pickups, prefer receivers closer to production.
var sorted = forwardDirection
? query.ThenByDescending(p => MinDistanceToProduction(p.CurrentCell, state, cargoType))
: query.ThenBy(p => MinDistanceToProduction(p.CurrentCell, state, cargoType));
return sorted.ThenBy(p => p.PlacementOrder).ToList();
} }
private static DemandState? GetAdjacentCompatibleDemand( private static DemandState? GetAdjacentCompatibleDemand(
@ -131,35 +135,20 @@ public static class TransferResolver
var adjacent = position.GetAdjacent4(state.Width, state.Height); var adjacent = position.GetAdjacent4(state.Width, state.Height);
return state.Demands.Values return state.Demands.Values
.Where(d => d.Cargo == cargoType .Where(d => !d.IsSatisfied
&& d.Cargo == cargoType
&& adjacent.Contains(d.Position)) && adjacent.Contains(d.Position))
.FirstOrDefault(); .FirstOrDefault();
} }
/// <summary> private static int MinDistanceToProduction(Coords cell, BoardState state, CargoType? cargoType = null)
/// Returns a sort key (0-3) based on cardinal direction from center to piece.
/// In y-up coordinates, clockwise from 0° (right):
/// right(1,0)=0, up(0,1)=1, left(-1,0)=2, down(0,-1)=3
/// On even turns, start from right (0°). On odd turns, start from left (180°).
/// </summary>
private static int ClockwiseOrder(Coords pieceCell, Coords center, int turnNumber)
{ {
int dx = pieceCell.Col - center.Col; var productions = cargoType != null
int dy = pieceCell.Row - center.Row; ? state.Productions.Where(kv => kv.Value.Cargo == cargoType).Select(kv => kv.Key)
: state.Productions.Keys;
int baseOrder = (dx, dy) switch var prodList = productions.ToList();
{ if (prodList.Count == 0) return int.MaxValue;
(1, 0) => 0, // right return prodList.Min(p => cell.ManhattanDistance(p));
(0, 1) => 1, // up (y-up)
(-1, 0) => 2, // left
(0, -1) => 3, // down (y-up)
_ => 4 // non-adjacent, shouldn't happen
};
// Odd turns: rotate by 2 (start from left instead of right)
if (turnNumber % 2 == 1)
baseOrder = (baseOrder + 2) % 4;
return baseOrder;
} }
} }

View file

@ -1 +0,0 @@
uid://c55a21x5tw5bo

View file

@ -1 +0,0 @@
uid://uh7qhohnsxpa

View file

@ -1 +0,0 @@
uid://c3aghlujtx44f

View file

@ -11,41 +11,39 @@ public static class TurnExecutor
state.TurnNumber++; state.TurnNumber++;
changeList.Add(new TurnStartedEvent(state.TurnNumber)); changeList.Add(new TurnStartedEvent(state.TurnNumber));
// Sub-phase 1: PRODUCTION // Sub-phase 1: MOVEMENT
ExecuteProduction(state, changeList); ExecuteMovement(state, changeList);
// Sub-phase 2: TRANSFERS // Sub-phase 2: COLLISION DETECTION
var collisions = CollisionDetector.DetectCollisions(state.Pieces);
if (collisions.Count > 0)
{
foreach (var (idA, idB, cell) in collisions)
changeList.Add(new CollisionDetectedEvent(idA, idB, cell));
state.Phase = SimPhase.Collision;
changeList.Add(new TurnEndedEvent(state.TurnNumber));
return;
}
// Sub-phase 3: TRANSFERS
var transferEvents = TransferResolver.ResolveTransfers(state); var transferEvents = TransferResolver.ResolveTransfers(state);
changeList.AddRange(transferEvents); changeList.AddRange(transferEvents);
// Sub-phase 3: MOVEMENT // Sub-phase 4: PRODUCTION
ExecuteMovement(state, changeList); ExecuteProduction(state, changeList);
// Sub-phase 4: COLLISION RESOLUTION
var collisions = CollisionResolver.ResolveCollisions(state.Pieces);
foreach (var (survivor, destroyed, cell) in collisions)
{
foreach (var victim in destroyed)
{
state.Pieces.Remove(victim);
state.DestroyedPieces.Add(victim);
victim.Cargo = null;
changeList.Add(new PieceDestroyedEvent(
state.TurnNumber, victim.Id, survivor?.Id, cell));
}
}
// Check victory / defeat // Check victory / defeat
if (VictoryChecker.AllDemandsMet(state)) if (VictoryChecker.AllDemandsMet(state))
{ {
state.Phase = SimPhase.Victory; state.Phase = SimPhase.Victory;
changeList.Add(new VictoryEvent(state.TurnNumber, ComputeMetrics(state))); changeList.Add(new VictoryEvent(ComputeMetrics(state)));
} }
else if (VictoryChecker.AnyDeadlineExpired(state)) else if (VictoryChecker.AnyDeadlineExpired(state))
{ {
state.Phase = SimPhase.Defeat; state.Phase = SimPhase.Defeat;
foreach (var demand in VictoryChecker.GetExpiredDemands(state)) foreach (var demand in VictoryChecker.GetExpiredDemands(state))
changeList.Add(new DeadlineExpiredEvent(state.TurnNumber, demand.Position, demand.Name)); changeList.Add(new DeadlineExpiredEvent(demand.Position, demand.Name));
} }
changeList.Add(new TurnEndedEvent(state.TurnNumber)); changeList.Add(new TurnEndedEvent(state.TurnNumber));
@ -61,7 +59,7 @@ public static class TurnExecutor
{ {
piece.CurrentCell = to; piece.CurrentCell = to;
state.OccupiedCells.Add(to); state.OccupiedCells.Add(to);
changeList.Add(new PieceMovedEvent(state.TurnNumber, piece.Id, from, to)); changeList.Add(new PieceMovedEvent(piece.Id, from, to));
} }
} }
@ -69,15 +67,21 @@ public static class TurnExecutor
{ {
foreach (var (pos, prod) in state.Productions) foreach (var (pos, prod) in state.Productions)
{ {
state.ProductionBuffers[pos] = prod.Amount; if (state.ProductionBuffers[pos] != null)
changeList.Add(new CargoProducedEvent(state.TurnNumber, pos, prod.Cargo)); continue; // buffer already full
if (state.TurnNumber % prod.Interval == 0)
{
state.ProductionBuffers[pos] = prod.Cargo;
changeList.Add(new CargoProducedEvent(pos, prod.Cargo));
}
} }
} }
private static Metrics ComputeMetrics(BoardState state) private static Metrics ComputeMetrics(BoardState state)
{ {
return new Metrics( return new Metrics(
PiecesUsed: state.Pieces.Count + state.DestroyedPieces.Count, PiecesUsed: state.Pieces.Count,
TurnsTaken: state.TurnNumber, TurnsTaken: state.TurnNumber,
CellsOccupied: state.OccupiedCells.Count CellsOccupied: state.OccupiedCells.Count
); );

View file

@ -1 +0,0 @@
uid://c1bwhv57ykh2x

View file

@ -17,9 +17,9 @@ public class BoardBuilder
_height = height; _height = height;
} }
public BoardBuilder WithProduction(int col, int row, string name, CargoType cargo, int amount = 1) public BoardBuilder WithProduction(int col, int row, string name, CargoType cargo, int interval = 2)
{ {
_productions.Add(new ProductionDef(new Coords(col, row), name, cargo, amount)); _productions.Add(new ProductionDef(new Coords(col, row), name, cargo, interval));
return this; return this;
} }

View file

@ -1 +0,0 @@
uid://bowilag4t3fw7

View file

@ -1 +0,0 @@
uid://b6bmm28wyg8oq

View file

@ -42,6 +42,8 @@ public class LevelLoaderTests
Assert.Equal(new Coords(0, 0), prod.Position); Assert.Equal(new Coords(0, 0), prod.Position);
Assert.Equal("Scierie", prod.Name); Assert.Equal("Scierie", prod.Name);
Assert.Equal(CargoType.Wood, prod.Cargo); Assert.Equal(CargoType.Wood, prod.Cargo);
Assert.Equal(2, prod.Interval);
Assert.Single(level.Demands); Assert.Single(level.Demands);
var demand = level.Demands[0]; var demand = level.Demands[0];
Assert.Equal(new Coords(3, 0), demand.Position); Assert.Equal(new Coords(3, 0), demand.Position);

View file

@ -1 +0,0 @@
uid://c67yiwl47b1gq

View file

@ -0,0 +1,52 @@
using Chessistics.Engine.Model;
using Chessistics.Engine.Rules;
using Xunit;
namespace Chessistics.Tests.Rules;
public class CollisionDetectorTests
{
[Fact]
public void NoCollision_PiecesOnDifferentCells()
{
var pieces = new List<PieceState>
{
new(1, PieceKind.Rook, new Coords(0, 0), new Coords(1, 0), 0) { CurrentCell = new Coords(0, 0) },
new(2, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 1) { CurrentCell = new Coords(2, 0) }
};
var collisions = CollisionDetector.DetectCollisions(pieces);
Assert.Empty(collisions);
}
[Fact]
public void Collision_TwoPiecesSameCell()
{
var cell = new Coords(1, 0);
var pieces = new List<PieceState>
{
new(1, PieceKind.Rook, new Coords(0, 0), cell, 0) { CurrentCell = cell },
new(2, PieceKind.Rook, new Coords(2, 0), cell, 1) { CurrentCell = cell }
};
var collisions = CollisionDetector.DetectCollisions(pieces);
Assert.Single(collisions);
Assert.Equal((1, 2, cell), collisions[0]);
}
[Fact]
public void Collision_ThreePiecesSameCell()
{
var cell = new Coords(1, 0);
var pieces = new List<PieceState>
{
new(1, PieceKind.Rook, new Coords(0, 0), cell, 0) { CurrentCell = cell },
new(2, PieceKind.Rook, new Coords(2, 0), cell, 1) { CurrentCell = cell },
new(3, PieceKind.Rook, new Coords(3, 0), cell, 2) { CurrentCell = cell }
};
var collisions = CollisionDetector.DetectCollisions(pieces);
// 3 pairs: (1,2), (1,3), (2,3)
Assert.Equal(3, collisions.Count);
}
}

View file

@ -1,87 +0,0 @@
using Chessistics.Engine.Model;
using Chessistics.Engine.Rules;
using Xunit;
namespace Chessistics.Tests.Rules;
public class CollisionResolverTests
{
[Fact]
public void NoCollision_PiecesOnDifferentCells()
{
var pieces = new List<PieceState>
{
new(1, PieceKind.Rook, new Coords(0, 0), new Coords(1, 0), 0) { CurrentCell = new Coords(0, 0) },
new(2, PieceKind.Rook, new Coords(2, 0), new Coords(3, 0), 1) { CurrentCell = new Coords(2, 0) }
};
var results = CollisionResolver.ResolveCollisions(pieces);
Assert.Empty(results);
}
[Fact]
public void HigherStatus_Survives()
{
var cell = new Coords(1, 0);
var rook = new PieceState(1, PieceKind.Rook, new Coords(0, 0), cell, 0) { CurrentCell = cell };
var knight = new PieceState(2, PieceKind.Knight, new Coords(2, 0), cell, 1) { CurrentCell = cell };
var pieces = new List<PieceState> { rook, knight };
var results = CollisionResolver.ResolveCollisions(pieces);
Assert.Single(results);
var (survivor, destroyed, resultCell) = results[0];
Assert.Equal(rook, survivor);
Assert.Single(destroyed);
Assert.Equal(knight, destroyed[0]);
Assert.Equal(cell, resultCell);
}
[Fact]
public void SameStatusAndLevel_MutualDestruction()
{
var cell = new Coords(1, 0);
var pieces = new List<PieceState>
{
new(1, PieceKind.Knight, new Coords(0, 0), cell, 0) { CurrentCell = cell },
new(2, PieceKind.Bishop, new Coords(2, 0), cell, 1) { CurrentCell = cell }
};
var results = CollisionResolver.ResolveCollisions(pieces);
Assert.Single(results);
var (survivor, destroyed, _) = results[0];
Assert.Null(survivor);
Assert.Equal(2, destroyed.Count);
}
[Fact]
public void HigherLevel_SurvivesWhenSameStatus()
{
var cell = new Coords(1, 0);
var knightL2 = new PieceState(1, PieceKind.Knight, new Coords(0, 0), cell, 0, level: 2) { CurrentCell = cell };
var knightL1 = new PieceState(2, PieceKind.Knight, new Coords(2, 0), cell, 1, level: 1) { CurrentCell = cell };
var pieces = new List<PieceState> { knightL1, knightL2 };
var results = CollisionResolver.ResolveCollisions(pieces);
Assert.Single(results);
var (survivor, destroyed, _) = results[0];
Assert.Equal(knightL2, survivor);
Assert.Single(destroyed);
Assert.Equal(knightL1, destroyed[0]);
}
[Fact]
public void ThreePieces_StrongestSurvives_OthersDestroyed()
{
var cell = new Coords(1, 0);
var rook = new PieceState(1, PieceKind.Rook, new Coords(0, 0), cell, 0) { CurrentCell = cell };
var bishop = new PieceState(2, PieceKind.Bishop, new Coords(2, 0), cell, 1) { CurrentCell = cell };
var knight = new PieceState(3, PieceKind.Knight, new Coords(3, 0), cell, 2) { CurrentCell = cell };
var pieces = new List<PieceState> { rook, bishop, knight };
var results = CollisionResolver.ResolveCollisions(pieces);
Assert.Single(results);
var (survivor, destroyed, _) = results[0];
Assert.Equal(rook, survivor);
Assert.Equal(2, destroyed.Count);
}
}

View file

@ -1 +0,0 @@
uid://dvjr7naqe8v28

View file

@ -9,7 +9,7 @@ public class MoveValidatorTests
{ {
private BoardState EmptyBoard(int size = 5) private BoardState EmptyBoard(int size = 5)
=> new BoardBuilder(size, size) => new BoardBuilder(size, size)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(size - 1, 0, "D", CargoType.Wood, 1, 99) .WithDemand(size - 1, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 10) .WithStock(PieceKind.Rook, 10)
.WithStock(PieceKind.Bishop, 10) .WithStock(PieceKind.Bishop, 10)
@ -37,7 +37,7 @@ public class MoveValidatorTests
public void Rook_BlockedByWall() public void Rook_BlockedByWall()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(1, 0) .WithWall(1, 0)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)
@ -75,7 +75,7 @@ public class MoveValidatorTests
public void Rook_CannotLandOnWall() public void Rook_CannotLandOnWall()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(3, 2) .WithWall(3, 2)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)
@ -109,7 +109,7 @@ public class MoveValidatorTests
public void Bishop_BlockedByWall() public void Bishop_BlockedByWall()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(3, 3) .WithWall(3, 3)
.WithStock(PieceKind.Bishop, 5) .WithStock(PieceKind.Bishop, 5)
@ -142,7 +142,7 @@ public class MoveValidatorTests
public void Knight_JumpsOverWalls() public void Knight_JumpsOverWalls()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(1, 2) .WithWall(1, 2)
.WithWall(2, 1) .WithWall(2, 1)
@ -178,7 +178,7 @@ public class MoveValidatorTests
public void Knight_CannotLandOnWall() public void Knight_CannotLandOnWall()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(3, 4) .WithWall(3, 4)
.WithStock(PieceKind.Knight, 5) .WithStock(PieceKind.Knight, 5)
@ -192,7 +192,7 @@ public class MoveValidatorTests
public void StartCell_CannotBeWall() public void StartCell_CannotBeWall()
{ {
var board = new BoardBuilder(5, 5) var board = new BoardBuilder(5, 5)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithWall(2, 2) .WithWall(2, 2)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)

View file

@ -1 +0,0 @@
uid://rmx2djjvmcoj

View file

@ -12,7 +12,7 @@ public class TransferResolverTests
public void Production_GivesToAdjacentEmptyPiece() public void Production_GivesToAdjacentEmptyPiece()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.BuildState(); .BuildState();
@ -23,7 +23,7 @@ public class TransferResolverTests
board.Pieces.Add(piece); board.Pieces.Add(piece);
// Fill production buffer // Fill production buffer
board.ProductionBuffers[new Coords(0, 0)] = 1; board.ProductionBuffers[new Coords(0, 0)] = CargoType.Wood;
var events = TransferResolver.ResolveTransfers(board); var events = TransferResolver.ResolveTransfers(board);
@ -33,14 +33,14 @@ public class TransferResolverTests
&& ct.Type == CargoType.Wood); && ct.Type == CargoType.Wood);
Assert.Equal(CargoType.Wood, piece.Cargo); Assert.Equal(CargoType.Wood, piece.Cargo);
Assert.Equal(0, board.ProductionBuffers[new Coords(0, 0)]); Assert.Null(board.ProductionBuffers[new Coords(0, 0)]);
} }
[Fact] [Fact]
public void Production_DoesNotGiveToPieceWithCargo() public void Production_DoesNotGiveToPieceWithCargo()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.BuildState(); .BuildState();
@ -49,20 +49,20 @@ public class TransferResolverTests
piece.CurrentCell = new Coords(1, 0); piece.CurrentCell = new Coords(1, 0);
piece.Cargo = CargoType.Wood; // already carrying piece.Cargo = CargoType.Wood; // already carrying
board.Pieces.Add(piece); board.Pieces.Add(piece);
board.ProductionBuffers[new Coords(0, 0)] = 1; board.ProductionBuffers[new Coords(0, 0)] = CargoType.Wood;
var events = TransferResolver.ResolveTransfers(board); var events = TransferResolver.ResolveTransfers(board);
// No transfer from production — piece already has cargo // No transfer from production — piece already has cargo
Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.From == new Coords(0, 0)); Assert.DoesNotContain(events, e => e is CargoTransferredEvent ct && ct.From == new Coords(0, 0));
Assert.Equal(1, board.ProductionBuffers[new Coords(0, 0)]); Assert.Equal(CargoType.Wood, board.ProductionBuffers[new Coords(0, 0)]);
} }
[Fact] [Fact]
public void Piece_TransfersToAdjacentEmptyPiece() public void Piece_TransfersToAdjacentEmptyPiece()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.BuildState(); .BuildState();
@ -88,7 +88,7 @@ public class TransferResolverTests
public void Piece_DeliversToDemand() public void Piece_DeliversToDemand()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 3, 99) .WithDemand(3, 0, "D", CargoType.Wood, 3, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.BuildState(); .BuildState();
@ -111,7 +111,7 @@ public class TransferResolverTests
public void Piece_DoesNotDeliverWrongType() public void Piece_DoesNotDeliverWrongType()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 3, 99) // wants Wood .WithDemand(3, 0, "D", CargoType.Wood, 3, 99) // wants Wood
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.BuildState(); .BuildState();
@ -131,7 +131,7 @@ public class TransferResolverTests
public void HigherStatus_ReceivesFirst() public void HigherStatus_ReceivesFirst()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.WithStock(PieceKind.Knight, 3) .WithStock(PieceKind.Knight, 3)
@ -162,7 +162,7 @@ public class TransferResolverTests
public void HigherStatus_GivesFirst() public void HigherStatus_GivesFirst()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.WithStock(PieceKind.Knight, 3) .WithStock(PieceKind.Knight, 3)
@ -203,71 +203,41 @@ public class TransferResolverTests
} }
[Fact] [Fact]
public void TieBreaker_ClockwiseDirection_EvenTurn() public void TieBreaker_PlacementOrder()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99) .WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Knight, 5) .WithStock(PieceKind.Knight, 5)
.BuildState(); .BuildState();
// Giver at (1,1) with cargo, two receivers: right(2,1) and up(1,2) // Two knights (same status 3) with cargo, both adjacent to same empty receiver
// On even turn (TurnNumber=0), clockwise from right: right=0 < up=1 var knight1 = new PieceState(1, PieceKind.Knight, new Coords(2, 0), new Coords(0, 1), 0); // earlier
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 0); knight1.CurrentCell = new Coords(2, 0);
giver.CurrentCell = new Coords(1, 1); knight1.Cargo = CargoType.Wood;
giver.Cargo = CargoType.Wood;
var receiverRight = new PieceState(2, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 1); var knight2 = new PieceState(2, PieceKind.Knight, new Coords(2, 2), new Coords(0, 3), 1); // later
receiverRight.CurrentCell = new Coords(2, 1); // right of giver knight2.CurrentCell = new Coords(2, 2);
knight2.Cargo = CargoType.Wood;
var receiverUp = new PieceState(3, PieceKind.Rook, new Coords(1, 2), new Coords(1, 3), 2); var receiver = new PieceState(3, PieceKind.Knight, new Coords(2, 1), new Coords(0, 2), 2);
receiverUp.CurrentCell = new Coords(1, 2); // up of giver receiver.CurrentCell = new Coords(2, 1); // adjacent to both (2,0) and (2,2)
board.Pieces.AddRange([giver, receiverRight, receiverUp]); board.Pieces.AddRange([knight1, knight2, receiver]);
board.TurnNumber = 2; // even turn
var events = TransferResolver.ResolveTransfers(board); var events = TransferResolver.ResolveTransfers(board);
var transfer = events.OfType<CargoTransferredEvent>().First(); var transfer = events.OfType<CargoTransferredEvent>().First();
// On even turn, right(0) has priority over up(1) // knight1 is closer to production at (0,0): dist = 2, knight2: dist = 4
Assert.Equal(2, transfer.ReceivingPieceId); // receiverRight // So knight1 gives first due to proximity (tiebreaker before placement order)
} Assert.Equal(1, transfer.GivingPieceId);
[Fact]
public void TieBreaker_ClockwiseDirection_OddTurn()
{
var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood)
.WithDemand(3, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Knight, 5)
.BuildState();
// Same setup but on odd turn: left=0 < down=1 < right=2 < up=3
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 0);
giver.CurrentCell = new Coords(1, 1);
giver.Cargo = CargoType.Wood;
var receiverRight = new PieceState(2, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 1);
receiverRight.CurrentCell = new Coords(2, 1); // right of giver
var receiverLeft = new PieceState(3, PieceKind.Rook, new Coords(0, 1), new Coords(0, 2), 2);
receiverLeft.CurrentCell = new Coords(0, 1); // left of giver
board.Pieces.AddRange([giver, receiverRight, receiverLeft]);
board.TurnNumber = 1; // odd turn
var events = TransferResolver.ResolveTransfers(board);
var transfer = events.OfType<CargoTransferredEvent>().First();
// On odd turn, left(0) has priority over right(2)
Assert.Equal(3, transfer.ReceivingPieceId); // receiverLeft
} }
[Fact] [Fact]
public void Cargo_MovesOneHopPerTurn() public void Cargo_MovesOneHopPerTurn()
{ {
var board = new BoardBuilder(5, 4) var board = new BoardBuilder(5, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)
.BuildState(); .BuildState();
@ -299,7 +269,7 @@ public class TransferResolverTests
public void NoCrossTransfer_NonAdjacent() public void NoCrossTransfer_NonAdjacent()
{ {
var board = new BoardBuilder(5, 4) var board = new BoardBuilder(5, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 99) .WithDemand(4, 0, "D", CargoType.Wood, 1, 99)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)
.BuildState(); .BuildState();
@ -323,7 +293,7 @@ public class TransferResolverTests
public void DemandPriority_OverPieceReceiver() public void DemandPriority_OverPieceReceiver()
{ {
var board = new BoardBuilder(4, 4) var board = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(2, 0, "D", CargoType.Wood, 3, 99) .WithDemand(2, 0, "D", CargoType.Wood, 3, 99)
.WithStock(PieceKind.Rook, 5) .WithStock(PieceKind.Rook, 5)
.BuildState(); .BuildState();

View file

@ -1 +0,0 @@
uid://7yqfkoottaie

View file

@ -13,7 +13,7 @@ public class FullLevelTests
// GDD Level 1: 4x4, Scierie(0,0) → Depot(3,0), 3 Rooks // GDD Level 1: 4x4, Scierie(0,0) → Depot(3,0), 3 Rooks
// Solution: single rook relay at (1,0)↔(2,0) // Solution: single rook relay at (1,0)↔(2,0)
var level = new BoardBuilder(4, 4) var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30) .WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.Build(); .Build();
@ -38,7 +38,7 @@ public class FullLevelTests
// Bishop(3,2↔4,3), G(4,3↔5,3) // Bishop(3,2↔4,3), G(4,3↔5,3)
// Total needed: 6 Rooks + 1 Bishop // Total needed: 6 Rooks + 1 Bishop
var level = new BoardBuilder(6, 6) var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(5, 0, "Depot Royal", CargoType.Wood, 2, 50) .WithDemand(5, 0, "Depot Royal", CargoType.Wood, 2, 50)
.WithDemand(5, 4, "Caserne", CargoType.Wood, 2, 50) .WithDemand(5, 4, "Caserne", CargoType.Wood, 2, 50)
.WithStock(PieceKind.Rook, 6) .WithStock(PieceKind.Rook, 6)
@ -83,8 +83,8 @@ public class FullLevelTests
// K2(2,0↔1,2), S3(1,2↔1,3), S4(1,3↔1,4), S5(1,4↔0,4) // K2(2,0↔1,2), S3(1,2↔1,3), S4(1,3↔1,4), S5(1,4↔0,4)
// Total: 10 Rooks + 2 Knights // Total: 10 Rooks + 2 Knights
var level = new BoardBuilder(6, 6) var level = new BoardBuilder(6, 6)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithProduction(5, 0, "Carriere", CargoType.Stone) .WithProduction(5, 0, "Carriere", CargoType.Stone, 2)
.WithDemand(5, 5, "Depot Royal", CargoType.Wood, 2, 60) .WithDemand(5, 5, "Depot Royal", CargoType.Wood, 2, 60)
.WithDemand(0, 5, "Forge", CargoType.Stone, 2, 60) .WithDemand(0, 5, "Forge", CargoType.Stone, 2, 60)
.WithWall(2, 2).WithWall(2, 3).WithWall(2, 4).WithWall(3, 4).WithWall(4, 4) .WithWall(2, 2).WithWall(2, 3).WithWall(2, 4).WithWall(3, 4).WithWall(4, 4)
@ -118,7 +118,7 @@ public class FullLevelTests
public void Level1_InsufficientPieces_NoVictory() public void Level1_InsufficientPieces_NoVictory()
{ {
var level = new BoardBuilder(4, 4) var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 3, "Depot Royal", CargoType.Wood, 3, 5) .WithDemand(3, 3, "Depot Royal", CargoType.Wood, 3, 5)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.Build(); .Build();

View file

@ -1 +0,0 @@
uid://bxu33tvxk3e0d

View file

@ -11,7 +11,7 @@ public class GameSimTests
private SimHelper CreateLevel1Sim() private SimHelper CreateLevel1Sim()
{ {
var level = new BoardBuilder(4, 4) var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 2)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30) .WithDemand(3, 0, "Depot Royal", CargoType.Wood, 3, 30)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.Build(); .Build();
@ -120,17 +120,18 @@ public class GameSimTests
} }
[Fact] [Fact]
public void Production_GeneratesEveryTurn() public void Production_GeneratesOnInterval()
{ {
var sim = CreateLevel1Sim(); var sim = CreateLevel1Sim();
// Place rook adjacent to production so it picks up cargo, freeing the buffer
sim.Place(PieceKind.Rook, (0, 0), (1, 0)); sim.Place(PieceKind.Rook, (0, 0), (1, 0));
sim.Start(); sim.Start();
var allEvents = sim.StepN(6); var allEvents = sim.StepN(6);
var prodEvents = allEvents.OfType<CargoProducedEvent>().ToList(); var prodEvents = allEvents.OfType<CargoProducedEvent>().ToList();
// Production fires every turn // With interval 2, produces on turns 2, 4, 6 (buffer freed each time by adjacent piece)
Assert.Equal(6, prodEvents.Count); Assert.True(prodEvents.Count >= 2, $"Expected at least 2 productions, got {prodEvents.Count}");
} }
[Fact] [Fact]
@ -138,7 +139,7 @@ public class GameSimTests
{ {
// Tiny level: prod adjacent to demand, just need one piece to relay // Tiny level: prod adjacent to demand, just need one piece to relay
var level = new BoardBuilder(3, 1) var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 1)
.WithDemand(2, 0, "D", CargoType.Wood, 1, 30) .WithDemand(2, 0, "D", CargoType.Wood, 1, 30)
.WithStock(PieceKind.Rook, 2) .WithStock(PieceKind.Rook, 2)
.Build(); .Build();
@ -158,7 +159,7 @@ public class GameSimTests
{ {
// Demand with very tight deadline, piece placed far from demand // Demand with very tight deadline, piece placed far from demand
var level = new BoardBuilder(4, 4) var level = new BoardBuilder(4, 4)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 2)
.WithDemand(3, 3, "D", CargoType.Wood, 10, 3) // need 10 in 3 turns — impossible .WithDemand(3, 3, "D", CargoType.Wood, 10, 3) // need 10 in 3 turns — impossible
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.Build(); .Build();

View file

@ -1 +0,0 @@
uid://dx5nefi0k4vpb

View file

@ -19,7 +19,7 @@ public class SolvabilityTests
// Delivery happens when rook returns to (1,0), adjacent to demand at (2,0). // Delivery happens when rook returns to (1,0), adjacent to demand at (2,0).
// Wait — (1,0) is adjacent to (2,0) ✓ so delivery from (1,0). // Wait — (1,0) is adjacent to (2,0) ✓ so delivery from (1,0).
var level = new BoardBuilder(3, 1) var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(2, 0, "Depot", CargoType.Wood, 2, 30) .WithDemand(2, 0, "Depot", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.Build(); .Build();
@ -42,7 +42,7 @@ public class SolvabilityTests
// Odd turns: A@(2,0) B@(3,0) C@(4,0) // Odd turns: A@(2,0) B@(3,0) C@(4,0)
// Even turns: A@(1,0) B@(2,0) C@(3,0) // Even turns: A@(1,0) B@(2,0) C@(3,0)
var level = new BoardBuilder(5, 2) var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.Build(); .Build();
@ -72,7 +72,7 @@ public class SolvabilityTests
// Rook B(0,1↔0,2): picks up at (0,1), delivers to D2 from (0,1). // Rook B(0,1↔0,2): picks up at (0,1), delivers to D2 from (0,1).
// Both rooks compete for the same buffer; A gets priority (placed first). // Both rooks compete for the same buffer; A gets priority (placed first).
var level = new BoardBuilder(4, 3) var level = new BoardBuilder(4, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(2, 0, "Depot Royal", CargoType.Wood, 2, 30) .WithDemand(2, 0, "Depot Royal", CargoType.Wood, 2, 30)
.WithDemand(0, 2, "Caserne", CargoType.Wood, 2, 30) .WithDemand(0, 2, "Caserne", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 2) .WithStock(PieceKind.Rook, 2)
@ -100,8 +100,8 @@ public class SolvabilityTests
// Row 1: Prod_Stone(0,1) — Rook B(1,1↔2,1) — Demand_Stone(3,1) // Row 1: Prod_Stone(0,1) — Rook B(1,1↔2,1) — Demand_Stone(3,1)
// Proves two cargo types flow independently to their matching demands. // Proves two cargo types flow independently to their matching demands.
var level = new BoardBuilder(4, 2) var level = new BoardBuilder(4, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithProduction(0, 1, "Carriere", CargoType.Stone) .WithProduction(0, 1, "Carriere", CargoType.Stone, 1)
.WithDemand(3, 0, "Depot Royal", CargoType.Wood, 2, 30) .WithDemand(3, 0, "Depot Royal", CargoType.Wood, 2, 30)
.WithDemand(3, 1, "Forge", CargoType.Stone, 2, 30) .WithDemand(3, 1, "Forge", CargoType.Stone, 2, 30)
.WithStock(PieceKind.Rook, 2) .WithStock(PieceKind.Rook, 2)
@ -133,7 +133,7 @@ public class SolvabilityTests
// Even turns: Rook@(0,1), Bishop@(1,1) — adjacent, transfer. // Even turns: Rook@(0,1), Bishop@(1,1) — adjacent, transfer.
// Odd turns: Rook@(0,0), Bishop@(2,2) — bishop delivers. // Odd turns: Rook@(0,0), Bishop@(2,2) — bishop delivers.
var level = new BoardBuilder(4, 3) var level = new BoardBuilder(4, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(2, 1, "Depot", CargoType.Wood, 2, 30) .WithDemand(2, 1, "Depot", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.WithStock(PieceKind.Bishop, 1) .WithStock(PieceKind.Bishop, 1)
@ -160,7 +160,7 @@ public class SolvabilityTests
// Even turns: Rook@(1,0), Knight@(1,1) — adjacent, transfer. // Even turns: Rook@(1,0), Knight@(1,1) — adjacent, transfer.
// Odd turns: Knight@(3,0), adjacent to demand — delivers. // Odd turns: Knight@(3,0), adjacent to demand — delivers.
var level = new BoardBuilder(5, 3) var level = new BoardBuilder(5, 3)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 30) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 30)
.WithWall(2, 0).WithWall(2, 1).WithWall(2, 2) .WithWall(2, 0).WithWall(2, 1).WithWall(2, 2)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
@ -184,7 +184,7 @@ public class SolvabilityTests
{ {
// 3x1: single rook relay, verify PiecesUsed, TurnsTaken, CellsOccupied. // 3x1: single rook relay, verify PiecesUsed, TurnsTaken, CellsOccupied.
var level = new BoardBuilder(3, 1) var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 1)
.WithDemand(2, 0, "D", CargoType.Wood, 2, 30) .WithDemand(2, 0, "D", CargoType.Wood, 2, 30)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.Build(); .Build();
@ -208,7 +208,7 @@ public class SolvabilityTests
// Two rooks sharing a relay point never collide. // Two rooks sharing a relay point never collide.
// A(1,0↔2,0), B(2,0↔3,0) — share cell (2,0) but occupy it on alternate turns. // A(1,0↔2,0), B(2,0↔3,0) — share cell (2,0) but occupy it on alternate turns.
var level = new BoardBuilder(5, 2) var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 1)
.WithDemand(4, 0, "D", CargoType.Wood, 1, 40) .WithDemand(4, 0, "D", CargoType.Wood, 1, 40)
.WithStock(PieceKind.Rook, 2) .WithStock(PieceKind.Rook, 2)
.Build(); .Build();
@ -220,7 +220,7 @@ public class SolvabilityTests
var allEvents = sim.StepN(20); var allEvents = sim.StepN(20);
Assert.DoesNotContain(allEvents, e => e is PieceDestroyedEvent); Assert.DoesNotContain(allEvents, e => e is CollisionDetectedEvent);
} }
[Fact] [Fact]
@ -233,8 +233,8 @@ public class SolvabilityTests
// With CargoFilter, A's start (1,0) is adjacent to prod_Wood(0,0), // With CargoFilter, A's start (1,0) is adjacent to prod_Wood(0,0),
// so A is filtered to Wood and ignores Stone. // so A is filtered to Wood and ignores Stone.
var level = new BoardBuilder(4, 1) var level = new BoardBuilder(4, 1)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithProduction(3, 0, "Carriere", CargoType.Stone) .WithProduction(3, 0, "Carriere", CargoType.Stone, 1)
.WithDemand(2, 0, "D_Wood", CargoType.Wood, 2, 20) .WithDemand(2, 0, "D_Wood", CargoType.Wood, 2, 20)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.Build(); .Build();
@ -261,7 +261,7 @@ public class SolvabilityTests
// 5x2: chain of 3 rooks, first adjacent to Wood production. // 5x2: chain of 3 rooks, first adjacent to Wood production.
// All should inherit Wood filter via relay chain propagation. // All should inherit Wood filter via relay chain propagation.
var level = new BoardBuilder(5, 2) var level = new BoardBuilder(5, 2)
.WithProduction(0, 0, "Scierie", CargoType.Wood) .WithProduction(0, 0, "Scierie", CargoType.Wood, 1)
.WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40) .WithDemand(4, 0, "Depot", CargoType.Wood, 2, 40)
.WithStock(PieceKind.Rook, 3) .WithStock(PieceKind.Rook, 3)
.Build(); .Build();
@ -282,7 +282,7 @@ public class SolvabilityTests
{ {
// Stepping from Edit phase should auto-start without needing Start command. // Stepping from Edit phase should auto-start without needing Start command.
var level = new BoardBuilder(3, 1) var level = new BoardBuilder(3, 1)
.WithProduction(0, 0, "P", CargoType.Wood) .WithProduction(0, 0, "P", CargoType.Wood, 1)
.WithDemand(2, 0, "D", CargoType.Wood, 1, 30) .WithDemand(2, 0, "D", CargoType.Wood, 1, 30)
.WithStock(PieceKind.Rook, 1) .WithStock(PieceKind.Rook, 1)
.Build(); .Build();

View file

@ -1 +0,0 @@
uid://ciq3nnqbtumxg

View file

@ -51,8 +51,8 @@ Le plateau est un damier avec des cases claires et sombres alternees. Chaque cas
| **Case claire** | Carre clair du damier | Traversable normalement | | **Case claire** | Carre clair du damier | Traversable normalement |
| **Case sombre** | Carre sombre du damier | Traversable (les Fous sont limites a une couleur) | | **Case sombre** | Carre sombre du damier | Traversable (les Fous sont limites a une couleur) |
| **Mur** | Case barree, gris fonce | Bloque toutes les pieces sauf le Cavalier (qui saute) | | **Mur** | Case barree, gris fonce | Bloque toutes les pieces sauf le Cavalier (qui saute) |
| **Production** | Icone ressource + nom (ex: "Scierie") | Produit M cargaisons tous les N coups (M=1 dans le proto, futur: 2-4). Le buffer est ecrase a chaque production — les cargaisons non recuperees sont perdues. Donne automatiquement aux pieces adjacentes disponibles. | | **Production** | Icone ressource + nom (ex: "Scierie") | Produit 1 cargaison tous les N coups. Donne automatiquement a une piece adjacente disponible. |
| **Demande** | Icone cible + nom (ex: "Depot") + jauge | Recoit la cargaison d'une piece adjacente qui arrive avec un colis compatible. Accepte toujours un colis du bon type, meme si l'objectif est deja atteint. | | **Demande** | Icone cible + nom (ex: "Depot") + jauge | Recoit la cargaison d'une piece adjacente qui arrive avec un colis compatible. |
### 2.3 Cargaison ### 2.3 Cargaison
@ -85,22 +85,7 @@ C'est tout. Pas de programmation, pas de route multi-etapes. La piece fait l'all
### 3.2 Pieces disponibles dans le prototype ### 3.2 Pieces disponibles dans le prototype
4 types. Chaque piece a un **niveau** (I, II, III…) qui determine sa puissance relative au sein d'un meme type. Dans le prototype, toutes les pieces sont de niveau fixe — le systeme de niveaux sera exploite dans les versions futures. 3 types, niveau unique :
4 types :
#### Pion
```
X
X [Pion] X
X
```
- Se deplace de **1 case** en ligne droite (horizontal ou vertical)
- Ne peut pas traverser les murs ni les autres pieces
- Statut social : **1** (le plus bas — recoit et donne en dernier)
- Piece la moins chere, utile pour combler les maillons courts
#### Tour (niveau II) #### Tour (niveau II)
@ -145,15 +130,13 @@ C'est tout. Pas de programmation, pas de route multi-etapes. La piece fait l'all
- **Saute par-dessus** les murs et les autres pieces - **Saute par-dessus** les murs et les autres pieces
- Statut social : **3** - Statut social : **3**
> A statut egal, la piece de **niveau le plus eleve** est consideree superieure (ex: Tour II > Tour I, Fou III > Cavalier II). En cas d'egalite parfaite, le departage se fait par **direction en sens horaire** depuis la piece qui donne (voir 4.3). > A statut egal (Fou et Cavalier = 3), la piece la plus proche de la production a la priorite. Si egalite parfaite, la piece la plus anciennement placee a la priorite.
### 3.3 Occupation et collision ### 3.3 Occupation et blocage
- Chaque piece **occupe sa case actuelle** (depart ou arrivee selon le coup) - Chaque piece **occupe sa case actuelle** (depart ou arrivee selon le coup)
- Une piece bloque le passage des autres pieces (sauf le Cavalier qui saute) - Une piece bloque le passage des autres pieces (sauf le Cavalier qui saute)
- Si deux pieces arrivent sur la meme case apres le mouvement, la piece de **statut le plus eleve** detruit l'autre. A statut egal, le **niveau** departage. A egalite parfaite (meme statut ET meme niveau), **destruction mutuelle**. - Deux pieces ne peuvent **jamais** occuper la meme case au meme coup
- La piece survivante reste sur la case avec sa cargaison intacte. La cargaison des pieces detruites est perdue.
- Les pieces detruites sont restaurees quand le joueur arrete la simulation (retour en mode edition).
--- ---
@ -176,30 +159,21 @@ Un transfert se produit quand, a la **fin d'un coup** (une fois que toutes les p
Le transfert est **instantane** : le colis passe d'une entite a l'autre entre deux coups. Le transfert est **instantane** : le colis passe d'une entite a l'autre entre deux coups.
### 4.3 Priorite et departage ### 4.3 Priorite par statut social
Quand plusieurs transferts sont possibles au meme point, la priorite determine l'ordre : Quand plusieurs transferts sont possibles au meme point, le **statut social** determine l'ordre :
**Chaine de priorite** : statut social (desc) → niveau de piece (desc) → direction en sens horaire. **Regle de don** : parmi les pieces avec colis adjacentes a un meme receveur, celle de **statut le plus eleve donne en premier**.
**Regle de don** : parmi les pieces avec colis adjacentes a un meme receveur, celle de priorite la plus elevee donne en premier. **Regle de reception** : parmi les pieces sans colis adjacentes a un meme donneur, celle de **statut le plus eleve recoit en premier**.
**Regle de reception** : parmi les pieces sans colis adjacentes a un meme donneur, celle de priorite la plus elevee recoit en premier.
``` ```
Hierarchie de statut social (proto) : Hierarchie de statut social (proto) :
Tour 5 Tour 5
Fou 3 Fou 3
Cavalier 3 Cavalier 3
Pion 1
``` ```
**Departage par direction** (en y-up, sens horaire) :
- Coups pairs : priorite depuis 0° (droite) → droite, haut, gauche, bas
- Coups impairs : priorite depuis 180° (gauche) → gauche, bas, droite, haut
Cette alternance empeche un biais permanent vers une direction et cree des patterns de routage dynamiques.
**Exemple** : **Exemple** :
``` ```
Tour (colis) ─adjacent─ case vide ─adjacent─ Cavalier (vide) Tour (colis) ─adjacent─ case vide ─adjacent─ Cavalier (vide)
@ -243,33 +217,30 @@ Gerer l'espace sur le plateau pour eviter les interferences EST le puzzle. Le jo
### 5.1 Sequence d'un coup ### 5.1 Sequence d'un coup
A chaque coup, toutes les pieces jouent chaque etape **simultanement**, dans cet ordre : A chaque coup, dans cet ordre :
``` ```
1. PRODUCTION : les cases de production remplissent leur buffer 1. MOUVEMENT : toutes les pieces bougent simultanement
(M cargaisons, ecrase le buffer precedent — les restes sont perdus)
2. TRANSFERTS : tous les transferts automatiques se resolvent
(productions → pieces, pieces → pieces, pieces → demandes)
En respectant la chaine de priorite (statut → niveau → direction)
3. MOUVEMENT : toutes les pieces bougent simultanement
(chaque piece avance de Depart→Arrivee ou de Arrivee→Depart) (chaque piece avance de Depart→Arrivee ou de Arrivee→Depart)
4. RESOLUTION DE COLLISION : si deux pieces sont sur la meme case, 2. DETECTION DE COLLISION : si deux pieces sont sur la meme case → erreur
la plus forte detruit les autres (voir 3.3)
3. TRANSFERTS : tous les transferts automatiques se resolvent
(productions → pieces, pieces → pieces, pieces → demandes)
En respectant l'ordre de statut social
4. PRODUCTION : les cases de production generent un colis
(si elles n'en ont pas deja un en attente)
``` ```
### 5.2 Collisions ### 5.2 Collisions
Quand deux pieces ou plus occupent la meme case apres le mouvement : Deux pieces ne peuvent pas occuper la meme case au meme coup. Si cela arrive :
- La piece de **statut le plus eleve** survit, les autres sont **detruites** - Les deux pieces clignotent en rouge
- A statut egal, le **niveau** departage (niveau superieur survit) - La simulation se met en **pause**
- A egalite parfaite (meme statut ET meme niveau), **destruction mutuelle** (aucun survivant) - Le joueur doit reorganiser ses pieces (revenir en mode edition)
- La piece survivante conserve sa cargaison. Les cargaisons detruites sont perdues.
- La simulation **continue** (pas de pause automatique)
Les collisions deviennent un element strategique — le joueur doit anticiper les croisements de trajectoires et espacer ses chaines pour les eviter. Les collisions sont le signal que les chaines sont mal agencees. Le joueur doit repenser l'espacement ou le timing (pieces de portees differentes arrivent a des moments differents).
### 5.3 Condition de victoire ### 5.3 Condition de victoire
@ -435,7 +406,7 @@ Le joueur peut arreter la simulation a tout moment, reorganiser, et relancer.
--- ---
## 8. Les 6 niveaux du prototype ## 8. Les 3 niveaux du prototype
### Niveau 1 — "Premier Convoi" ### Niveau 1 — "Premier Convoi"
@ -588,86 +559,6 @@ Le Cavalier saute le mur en L. Il peut connecter les deux cotes du plateau la ou
--- ---
### Niveau 4 — "Le Carrefour"
**Intention** : premier plateau 8x8, deux cargaisons en diagonale, un bloc de murs au centre force le contournement.
```
8 . . . . . . . .
7 [D2] . . . . . . . Forge — 3 Pierre en 40 coups
6 . . . . . . . .
5 . . . ## . . . .
4 . . . ## . . . .
3 . . . . . . . .
2 . . . . . . . .
1 [S1] . . . . . . [D1] Depot Royal — 3 Bois en 40 coups
a b c d e f g h
[S2] Carriere (h8)
```
- Plateau : **8x8**
- S1 = Scierie (a1, Bois), S2 = Carriere (h8, Pierre)
- D1 = Depot Royal (h1, 3 Bois/40c), D2 = Forge (a8, 3 Pierre/40c)
- Murs : bloc 2x2 au centre (d4, e5, d5, e4)
- Stock : 8 Pions, 4 Tours, 2 Fous, 2 Cavaliers
**L'enjeu** : les routes Bois (a1→h1) et Pierre (h8→a8) se croisent en diagonale. Le bloc central empeche la ligne droite. Le joueur decouvre le Pion comme maillon economique.
---
### Niveau 5 — "Le Labyrinthe"
**Intention** : des murs verticaux creent des couloirs etroits. Les Cavaliers sont essentiels pour enjamber les obstacles.
```
6 [S2] . # . # . # . Carriere (a6)
5 . . # . # . # .
4 . . # . # . . [D1] Depot Royal — 3 Bois en 50 coups
3 . . # . . . # .
2 . . . . # . # .
1 [S1] . . . # . . [D2] Forge — 3 Pierre en 50 coups
a b c d e f g h
```
- Plateau : **8x6**
- S1 = Scierie (a1, Bois), S2 = Carriere (a6, Pierre)
- D1 = Depot Royal (h6, 3 Bois/50c), D2 = Forge (h1, 3 Pierre/50c)
- Murs : 3 colonnes partielles formant un labyrinthe
- Stock : 10 Pions, 4 Tours, 2 Fous, 3 Cavaliers
**L'enjeu** : les murs fragmentent le plateau en couloirs. Seul le Cavalier saute par-dessus. Le joueur doit combiner Pions (relais courts dans les couloirs) et Cavaliers (ponts entre couloirs).
---
### Niveau 6 — "Trois Royaumes"
**Intention** : reseau a 3 productions et 3 demandes, plateau 10x8. Le joueur gere un vrai reseau logistique.
```
8 [S2] . . # . . # . . [D2] Forge — 3 Pierre en 50 coups
7 . . . # . . # . . .
6 . . . # ## . # . . .
5 . . . . . . . . . .
4 . . . # ## . # . . [S3] Scierie Est (j4, Bois)
3 . . . # . . # . . .
2 . . . . . . . . . .
1 [S1] . . . [D3] . . . . [D1] Depot Royal — 3 Bois en 50 coups
a b c d e f g h i j
```
- Plateau : **10x8**
- S1 = Scierie (a1, Bois), S2 = Carriere (a8, Pierre), S3 = Scierie Est (j4, Bois)
- D1 = Depot Royal (j1, 3 Bois/50c), D2 = Forge (j8, 3 Pierre/50c), D3 = Chantier (e8, 3 Bois/50c)
- Murs : deux colonnes avec pont horizontal
- Stock : 14 Pions, 6 Tours, 3 Fous, 4 Cavaliers
**L'enjeu** : le joueur doit decider comment repartir les productions entre les demandes. S1 et S3 produisent du Bois mais sont loin l'une de l'autre. Les murs creent trois "royaumes" et le joueur doit construire un reseau d'interconnexion.
---
## 9. Direction artistique (prototype) ## 9. Direction artistique (prototype)
Le prototype vise la lisibilite. Le prototype vise la lisibilite.
@ -782,13 +673,7 @@ Chessistics/
|----------|---------|----------------| |----------|---------|----------------|
| Adjacence pour transfert ? | 4-connecte (bords) vs 8-connecte (bords + coins) | **4-connecte** — plus de contrainte = plus de puzzle | | Adjacence pour transfert ? | 4-connecte (bords) vs 8-connecte (bords + coins) | **4-connecte** — plus de contrainte = plus de puzzle |
| La piece tourne a vide ? | Oui (aller-retour permanent) vs attend un colis | **Oui, tourne en permanence** — plus visuel, plus simple | | La piece tourne a vide ? | Oui (aller-retour permanent) vs attend un colis | **Oui, tourne en permanence** — plus visuel, plus simple |
| Collision = destruction ? | Destruction (fort mange faible) vs erreur stricte (pause) | **Destruction** — la piece de plus haut statut/niveau survit, les autres sont detruites. Destruction mutuelle si egalite parfaite. | | Collision = erreur stricte ? | Stricte (pause) vs tolerante | **Stricte** — le joueur voit et corrige. Plus simple a implementer. |
| Sources infinies ? | Oui (production periodique sans stock) vs stock limite | **Production periodique infinie** — le proto teste le reseau, pas la gestion de stock | | Sources infinies ? | Oui (production periodique sans stock) vs stock limite | **Production periodique infinie** — le proto teste le reseau, pas la gestion de stock |
| Pieces fixes par niveau ? | Fixes (catalogue impose) vs achat libre | **Fixes** — plus facile a designer. L'achat/fabrication est post-proto. | | Pieces fixes par niveau ? | Fixes (catalogue impose) vs achat libre | **Fixes** — plus facile a designer. L'achat/fabrication est post-proto. |
| Egalite de statut social ? | Niveau > direction horaire | **Niveau puis direction horaire alternee** (pairs: depuis droite, impairs: depuis gauche) — deterministe, pas de biais permanent | | Egalite de statut social ? | Proximite > anciennete | **Proximite puis anciennete** — intuitif, pas de regle arbitraire |
---
## 12. Lore
Les pions sont en manque d'un roi. Ils vont se mettre a produire toutes les pieces intermediaires car ils en ont besoin pour aller chercher plus loin les ressources requises pour fabriquer le roi. A la fin, le roi execute tout le monde. Game over.

View file

@ -12,14 +12,9 @@ config_version=5
config/name="Chessistics" config/name="Chessistics"
run/main_scene="res://Scenes/Main.tscn" run/main_scene="res://Scenes/Main.tscn"
config/features=PackedStringArray("4.6", "C#", "GL Compatibility") config/features=PackedStringArray("4.6", "GL Compatibility")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[display]
window/size/viewport_width=1280
window/size/viewport_height=720
[dotnet] [dotnet]
project/assembly_name="Chessistics" project/assembly_name="Chessistics"