Compare commits
5 commits
d1926c2b4d
...
e1218b3eaa
| Author | SHA1 | Date | |
|---|---|---|---|
| e1218b3eaa | |||
| 4afe20842e | |||
| 672b831fc1 | |||
| a7280b1a5a | |||
| dd43df8820 |
99 changed files with 1963 additions and 876 deletions
91
.devcontainer/Dockerfile
Normal file
91
.devcontainer/Dockerfile
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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
|
||||||
30
.devcontainer/devcontainer.json
Normal file
30
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
134
.devcontainer/init-firewall.sh
Normal file
134
.devcontainer/init-firewall.sh
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
#!/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
1
.gitignore
vendored
|
|
@ -20,3 +20,4 @@ Thumbs.db
|
||||||
|
|
||||||
# Claude Code
|
# Claude Code
|
||||||
.claude/
|
.claude/
|
||||||
|
.idea
|
||||||
|
|
|
||||||
26
CLAUDE.md
Normal file
26
CLAUDE.md
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -5,13 +5,14 @@
|
||||||
"width": 4,
|
"width": 4,
|
||||||
"height": 4,
|
"height": 4,
|
||||||
"productions": [
|
"productions": [
|
||||||
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 }
|
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" }
|
||||||
],
|
],
|
||||||
"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": "rook", "count": 3 }
|
{ "kind": "pawn", "count": 4 },
|
||||||
|
{ "kind": "rook", "count": 2 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
"width": 6,
|
"width": 6,
|
||||||
"height": 6,
|
"height": 6,
|
||||||
"productions": [
|
"productions": [
|
||||||
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 }
|
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" }
|
||||||
],
|
],
|
||||||
"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,6 +13,7 @@
|
||||||
],
|
],
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"stock": [
|
"stock": [
|
||||||
|
{ "kind": "pawn", "count": 6 },
|
||||||
{ "kind": "rook", "count": 4 },
|
{ "kind": "rook", "count": 4 },
|
||||||
{ "kind": "bishop", "count": 1 }
|
{ "kind": "bishop", "count": 1 }
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
"width": 6,
|
"width": 6,
|
||||||
"height": 6,
|
"height": 6,
|
||||||
"productions": [
|
"productions": [
|
||||||
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood", "interval": 2 },
|
{ "col": 0, "row": 0, "name": "Scierie", "cargo": "wood" },
|
||||||
{ "col": 5, "row": 0, "name": "Carriere", "cargo": "stone", "interval": 2 }
|
{ "col": 5, "row": 0, "name": "Carriere", "cargo": "stone" }
|
||||||
],
|
],
|
||||||
"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,7 +20,8 @@
|
||||||
{ "col": 4, "row": 4 }
|
{ "col": 4, "row": 4 }
|
||||||
],
|
],
|
||||||
"stock": [
|
"stock": [
|
||||||
{ "kind": "rook", "count": 4 },
|
{ "kind": "pawn", "count": 6 },
|
||||||
|
{ "kind": "rook", "count": 6 },
|
||||||
{ "kind": "bishop", "count": 1 },
|
{ "kind": "bishop", "count": 1 },
|
||||||
{ "kind": "knight", "count": 2 }
|
{ "kind": "knight", "count": 2 }
|
||||||
]
|
]
|
||||||
|
|
|
||||||
27
Data/levels/level_04.json
Normal file
27
Data/levels/level_04.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
35
Data/levels/level_05.json
Normal file
35
Data/levels/level_05.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"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 }
|
||||||
|
]
|
||||||
|
}
|
||||||
35
Data/levels/level_06.json
Normal file
35
Data/levels/level_06.json
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"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
24
PLAN.md
|
|
@ -20,25 +20,13 @@
|
||||||
- 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: Surplus stock and puzzle difficulty tuning
|
## Phase 3: Pion, surplus stock, levels 4-6 (DONE)
|
||||||
|
|
||||||
**Goal**: Levels give more pieces than the minimum, creating genuine puzzle space.
|
- Pion: orthogonal range 1, status 1 (lowest), cheap relay maillon
|
||||||
|
- Surplus stock on all levels (more pieces than minimum solution)
|
||||||
- With forward-preferring transfers working, longer chains are viable.
|
- Levels 4-6: Le Carrefour (8x8), Le Labyrinthe (8x6), Trois Royaumes (10x8)
|
||||||
- Design levels where the player has choice: multiple valid solutions with different
|
- Production interval removed: all productions fire every turn
|
||||||
efficiency scores (PiecesUsed, TurnsTaken, CellsOccupied).
|
- GDD updated with Pion, 6 levels
|
||||||
- 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[gd_scene load_steps=2 format=3 uid="uid://main_scene"]
|
[gd_scene format=3 uid="uid://6j24v4md60t7"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://Scripts/Main.cs" id="1"]
|
[ext_resource type="Script" uid="uid://dygonjc0xhp15" path="res://Scripts/Main.cs" id="1"]
|
||||||
|
|
||||||
[node name="Main" type="Node2D"]
|
[node name="Main" type="Node2D" unique_id=2090714159]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
|
||||||
|
|
@ -6,82 +6,93 @@ 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);
|
||||||
int row = Mathf.FloorToInt(-localPos.Y / CellSize);
|
// Cell at row R has top-left Y = -R*CellSize, extending downward.
|
||||||
|
// 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 ClearHighlights()
|
public void SetHoverCell(Coords? coords)
|
||||||
{
|
{
|
||||||
foreach (var cell in _cells.Values)
|
foreach (var cell in _cells.Values)
|
||||||
cell.SetHighlight(false);
|
cell.SetHover(false);
|
||||||
}
|
|
||||||
|
|
||||||
public void HighlightCells(IEnumerable<Coords> cells, Color color)
|
if (coords != null && _cells.TryGetValue(coords.Value, out var cellView))
|
||||||
{
|
cellView.SetHover(true);
|
||||||
foreach (var coords in cells)
|
}
|
||||||
{
|
|
||||||
if (_cells.TryGetValue(coords, out var cellView))
|
public void ClearHighlights()
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Scripts/Board/BoardView.cs.uid
Normal file
1
Scripts/Board/BoardView.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://njfxxe08s5w
|
||||||
|
|
@ -9,6 +9,12 @@ 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");
|
||||||
|
|
@ -17,6 +23,9 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +35,8 @@ 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;
|
||||||
|
|
@ -44,14 +54,57 @@ 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);
|
||||||
|
|
@ -60,9 +113,29 @@ 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Scripts/Board/CellView.cs.uid
Normal file
1
Scripts/Board/CellView.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dt08gv2w0t3kj
|
||||||
|
|
@ -24,6 +24,7 @@ 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;
|
||||||
|
|
||||||
|
|
@ -32,10 +33,15 @@ public partial class InputMapper : Node
|
||||||
_boardView = boardView;
|
_boardView = boardView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetSnapshot(BoardSnapshot snapshot) => _snapshot = snapshot;
|
public void SetSnapshot(BoardSnapshot 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;
|
||||||
|
|
@ -50,6 +56,20 @@ 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)
|
||||||
|
|
@ -62,7 +82,9 @@ public partial class InputMapper : Node
|
||||||
|
|
||||||
if (mouseEvent.ButtonIndex == MouseButton.Left)
|
if (mouseEvent.ButtonIndex == MouseButton.Left)
|
||||||
{
|
{
|
||||||
HandleLeftClick(mouseEvent.GlobalPosition);
|
var localPos = _boardView.GetLocalMousePosition();
|
||||||
|
GD.Print($"[InputMapper] LEFT CLICK — localPos={localPos}, phase={_phase}");
|
||||||
|
HandleLeftClick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,12 +94,18 @@ public partial class InputMapper : Node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleLeftClick(Vector2 globalPos)
|
private void HandleLeftClick()
|
||||||
{
|
{
|
||||||
var localPos = _boardView.ToLocal(globalPos);
|
var localPos = _boardView.GetLocalMousePosition();
|
||||||
var coords = _boardView.PixelToCoords(localPos);
|
var coords = _boardView.PixelToCoords(localPos);
|
||||||
|
|
||||||
if (coords == null) return;
|
GD.Print($"[InputMapper] HandleLeftClick — localPos={localPos}, coords={coords}");
|
||||||
|
|
||||||
|
if (coords == null)
|
||||||
|
{
|
||||||
|
GD.Print("[InputMapper] coords is null — click outside board");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (_phase)
|
switch (_phase)
|
||||||
{
|
{
|
||||||
|
|
@ -97,13 +125,22 @@ public partial class InputMapper : Node
|
||||||
|
|
||||||
private void OnStartSelected(Coords start)
|
private void OnStartSelected(Coords start)
|
||||||
{
|
{
|
||||||
if (_selectedKind == null || _snapshot == null) return;
|
if (_selectedKind == null || _snapshot == null)
|
||||||
|
{
|
||||||
|
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) return;
|
if (boardState == null)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
@ -122,6 +159,7 @@ 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
|
||||||
|
|
@ -133,12 +171,8 @@ 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,
|
||||||
|
|
@ -151,7 +185,6 @@ 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];
|
||||||
|
|
|
||||||
1
Scripts/Input/InputMapper.cs.uid
Normal file
1
Scripts/Input/InputMapper.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cfpek0cba5h50
|
||||||
917
Scripts/Main.cs
917
Scripts/Main.cs
|
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
|
@ -15,424 +16,500 @@ 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!;
|
||||||
// Simulation timer
|
private PanelContainer _controlBarWrapper = null!;
|
||||||
private Godot.Timer _simTimer = null!;
|
private Camera2D _camera = null!;
|
||||||
private float _simInterval = 1.0f;
|
|
||||||
private bool _running;
|
// Simulation timer
|
||||||
|
private Godot.Timer _simTimer = null!;
|
||||||
private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json"];
|
private float _simInterval = 1.0f;
|
||||||
|
private bool _running;
|
||||||
private static readonly Color BackgroundColor = new("#2D2D2D");
|
private bool _panning;
|
||||||
|
|
||||||
public override void _Ready()
|
private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json", "level_04.json", "level_05.json", "level_06.json"];
|
||||||
{
|
|
||||||
RenderingServer.SetDefaultClearColor(BackgroundColor);
|
private const float SidePanelWidth = 280f;
|
||||||
|
private const float ControlBarHeight = 48f;
|
||||||
BuildSceneTree();
|
|
||||||
ConnectSignals();
|
private static readonly Color BackgroundColor = new("#2D2D2D");
|
||||||
ShowLevelSelect();
|
|
||||||
}
|
public override void _Ready()
|
||||||
|
{
|
||||||
private void BuildSceneTree()
|
RenderingServer.SetDefaultClearColor(BackgroundColor);
|
||||||
{
|
|
||||||
// Camera
|
BuildSceneTree();
|
||||||
var camera = new Camera2D { Enabled = true };
|
ConnectSignals();
|
||||||
AddChild(camera);
|
ShowLevelSelect();
|
||||||
|
}
|
||||||
// Board
|
|
||||||
_boardView = new BoardView();
|
public override void _UnhandledInput(InputEvent @event)
|
||||||
AddChild(_boardView);
|
{
|
||||||
|
if (@event is InputEventMouseButton mb)
|
||||||
// Input
|
{
|
||||||
_inputMapper = new InputMapper();
|
if (mb.ButtonIndex == MouseButton.Middle)
|
||||||
_inputMapper.Initialize(_boardView);
|
_panning = mb.Pressed;
|
||||||
AddChild(_inputMapper);
|
}
|
||||||
|
else if (@event is InputEventMouseMotion motion && _panning)
|
||||||
// Animator
|
{
|
||||||
_eventAnimator = new EventAnimator();
|
_camera.Position -= motion.Relative / _camera.Zoom;
|
||||||
AddChild(_eventAnimator);
|
}
|
||||||
|
}
|
||||||
// Sim timer
|
|
||||||
_simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval };
|
private void BuildSceneTree()
|
||||||
_simTimer.Timeout += OnSimTimerTick;
|
{
|
||||||
AddChild(_simTimer);
|
// Camera
|
||||||
|
_camera = new Camera2D { Enabled = true };
|
||||||
// UI Layer
|
AddChild(_camera);
|
||||||
_uiLayer = new CanvasLayer();
|
|
||||||
AddChild(_uiLayer);
|
// Board
|
||||||
|
_boardView = new BoardView();
|
||||||
// Level title
|
AddChild(_boardView);
|
||||||
_levelTitle = new Label
|
|
||||||
{
|
// Input
|
||||||
Position = new Vector2(10, 10),
|
_inputMapper = new InputMapper();
|
||||||
Text = "CHESSISTICS"
|
_inputMapper.Initialize(_boardView);
|
||||||
};
|
AddChild(_inputMapper);
|
||||||
_levelTitle.AddThemeFontSizeOverride("font_size", 20);
|
|
||||||
_uiLayer.AddChild(_levelTitle);
|
// Animator
|
||||||
|
_eventAnimator = new EventAnimator();
|
||||||
// Side panel (right)
|
AddChild(_eventAnimator);
|
||||||
var sidePanel = new VBoxContainer
|
|
||||||
{
|
// Sim timer
|
||||||
Position = new Vector2(700, 50),
|
_simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval };
|
||||||
CustomMinimumSize = new Vector2(200, 500)
|
_simTimer.Timeout += OnSimTimerTick;
|
||||||
};
|
AddChild(_simTimer);
|
||||||
|
|
||||||
_objectivePanel = new ObjectivePanel();
|
// --- UI Layer ---
|
||||||
sidePanel.AddChild(_objectivePanel);
|
_uiLayer = new CanvasLayer();
|
||||||
sidePanel.AddChild(new HSeparator());
|
AddChild(_uiLayer);
|
||||||
|
|
||||||
_pieceStockPanel = new PieceStockPanel();
|
// Root control anchored to viewport (required for child anchoring)
|
||||||
sidePanel.AddChild(_pieceStockPanel);
|
var uiRoot = new Control();
|
||||||
|
uiRoot.SetAnchorsPreset(Control.LayoutPreset.FullRect);
|
||||||
_detailPanel = new DetailPanel();
|
uiRoot.MouseFilter = Control.MouseFilterEnum.Ignore;
|
||||||
sidePanel.AddChild(_detailPanel);
|
_uiLayer.AddChild(uiRoot);
|
||||||
|
|
||||||
_uiLayer.AddChild(sidePanel);
|
// Level title (top-left)
|
||||||
|
_levelTitle = new Label { Text = "CHESSISTICS" };
|
||||||
// Control bar (bottom)
|
_levelTitle.SetAnchorsPreset(Control.LayoutPreset.TopLeft);
|
||||||
_controlBar = new ControlBar
|
_levelTitle.OffsetLeft = 16;
|
||||||
{
|
_levelTitle.OffsetTop = 12;
|
||||||
Position = new Vector2(10, 600)
|
_levelTitle.AddThemeFontSizeOverride("font_size", 20);
|
||||||
};
|
_levelTitle.MouseFilter = Control.MouseFilterEnum.Ignore;
|
||||||
_uiLayer.AddChild(_controlBar);
|
uiRoot.AddChild(_levelTitle);
|
||||||
|
|
||||||
// Metrics overlay (center)
|
// --- Side Panel (anchored to right edge) ---
|
||||||
_metricsOverlay = new MetricsOverlay
|
_sidePanel = new PanelContainer();
|
||||||
{
|
_sidePanel.AnchorLeft = 1.0f;
|
||||||
Position = new Vector2(200, 150),
|
_sidePanel.AnchorRight = 1.0f;
|
||||||
CustomMinimumSize = new Vector2(300, 250)
|
_sidePanel.AnchorTop = 0.0f;
|
||||||
};
|
_sidePanel.AnchorBottom = 1.0f;
|
||||||
_uiLayer.AddChild(_metricsOverlay);
|
_sidePanel.OffsetLeft = -SidePanelWidth;
|
||||||
|
_sidePanel.OffsetRight = 0;
|
||||||
// Level select screen
|
_sidePanel.OffsetTop = 0;
|
||||||
_levelSelectScreen = new LevelSelectScreen();
|
_sidePanel.OffsetBottom = -ControlBarHeight;
|
||||||
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
|
|
||||||
_uiLayer.AddChild(_levelSelectScreen);
|
var sidePanelStyle = new StyleBoxFlat
|
||||||
|
{
|
||||||
// Initialize animator
|
BgColor = new Color(0.14f, 0.14f, 0.16f, 0.97f),
|
||||||
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
|
BorderColor = new Color(0.25f, 0.25f, 0.28f),
|
||||||
}
|
BorderWidthLeft = 1,
|
||||||
|
ContentMarginLeft = 16,
|
||||||
private void ConnectSignals()
|
ContentMarginRight = 16,
|
||||||
{
|
ContentMarginTop = 16,
|
||||||
_levelSelectScreen.LevelSelected += OnLevelSelected;
|
ContentMarginBottom = 16
|
||||||
_pieceStockPanel.PieceSelected += OnPieceKindSelected;
|
};
|
||||||
_inputMapper.PlacementRequested += OnPlacementRequested;
|
_sidePanel.AddThemeStyleboxOverride("panel", sidePanelStyle);
|
||||||
_inputMapper.Cancelled += OnPlacementCancelled;
|
|
||||||
_controlBar.PlayPressed += OnPlay;
|
var sideScroll = new ScrollContainer
|
||||||
_controlBar.PausePressed += OnPause;
|
{
|
||||||
_controlBar.StepPressed += OnStep;
|
HorizontalScrollMode = ScrollContainer.ScrollMode.Disabled,
|
||||||
_controlBar.StopPressed += OnStop;
|
SizeFlagsVertical = Control.SizeFlags.ExpandFill
|
||||||
_controlBar.SpeedChanged += OnSpeedChanged;
|
};
|
||||||
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
|
|
||||||
_eventAnimator.VictoryReached += OnVictory;
|
var sideVBox = new VBoxContainer();
|
||||||
_eventAnimator.CollisionOccurred += OnCollision;
|
sideVBox.AddThemeConstantOverride("separation", 12);
|
||||||
_metricsOverlay.RetryPressed += OnRetry;
|
|
||||||
_metricsOverlay.NextLevelPressed += OnNextLevel;
|
_objectivePanel = new ObjectivePanel();
|
||||||
_detailPanel.RemoveRequested += OnRemoveRequested;
|
sideVBox.AddChild(_objectivePanel);
|
||||||
_inputMapper.CellClicked += OnCellClicked;
|
sideVBox.AddChild(new HSeparator());
|
||||||
}
|
|
||||||
|
_pieceStockPanel = new PieceStockPanel();
|
||||||
private void OnCellClicked(int col, int row)
|
sideVBox.AddChild(_pieceStockPanel);
|
||||||
{
|
|
||||||
if (_sim == null) return;
|
_detailPanel = new DetailPanel();
|
||||||
var snap = _sim.GetSnapshot();
|
sideVBox.AddChild(_detailPanel);
|
||||||
if (snap.Phase != SimPhase.Edit) return;
|
|
||||||
|
sideScroll.AddChild(sideVBox);
|
||||||
var coords = new Coords(col, row);
|
_sidePanel.AddChild(sideScroll);
|
||||||
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
|
uiRoot.AddChild(_sidePanel);
|
||||||
if (piece != null)
|
|
||||||
_detailPanel.ShowPiece(piece);
|
// --- Control Bar (anchored to bottom, left of side panel) ---
|
||||||
else
|
_controlBarWrapper = new PanelContainer();
|
||||||
_detailPanel.Hide();
|
_controlBarWrapper.AnchorLeft = 0.0f;
|
||||||
}
|
_controlBarWrapper.AnchorRight = 1.0f;
|
||||||
|
_controlBarWrapper.AnchorTop = 1.0f;
|
||||||
// --- Level Management ---
|
_controlBarWrapper.AnchorBottom = 1.0f;
|
||||||
|
_controlBarWrapper.OffsetTop = -ControlBarHeight;
|
||||||
private void ShowLevelSelect()
|
_controlBarWrapper.OffsetRight = -SidePanelWidth;
|
||||||
{
|
|
||||||
_levelSelectScreen.Visible = true;
|
var controlBarStyle = new StyleBoxFlat
|
||||||
_boardView.Visible = false;
|
{
|
||||||
}
|
BgColor = new Color(0.11f, 0.11f, 0.13f, 0.97f),
|
||||||
|
BorderColor = new Color(0.25f, 0.25f, 0.28f),
|
||||||
private void OnLevelSelected(int levelIndex)
|
BorderWidthTop = 1,
|
||||||
{
|
ContentMarginLeft = 12,
|
||||||
_currentLevelIndex = levelIndex;
|
ContentMarginRight = 12,
|
||||||
LoadLevel(levelIndex);
|
ContentMarginTop = 4,
|
||||||
}
|
ContentMarginBottom = 4
|
||||||
|
};
|
||||||
private void LoadLevel(int index)
|
_controlBarWrapper.AddThemeStyleboxOverride("panel", controlBarStyle);
|
||||||
{
|
|
||||||
if (index < 0 || index >= LevelFiles.Length) return;
|
_controlBar = new ControlBar();
|
||||||
|
_controlBarWrapper.AddChild(_controlBar);
|
||||||
var path = $"res://Data/levels/{LevelFiles[index]}";
|
uiRoot.AddChild(_controlBarWrapper);
|
||||||
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read);
|
|
||||||
if (file == null)
|
// --- Metrics Overlay (centered in board area) ---
|
||||||
{
|
var metricsCenter = new CenterContainer();
|
||||||
GD.PrintErr($"Cannot open level file: {path}");
|
metricsCenter.SetAnchorsPreset(Control.LayoutPreset.FullRect);
|
||||||
return;
|
metricsCenter.OffsetRight = -SidePanelWidth;
|
||||||
}
|
metricsCenter.OffsetBottom = -ControlBarHeight;
|
||||||
|
metricsCenter.MouseFilter = Control.MouseFilterEnum.Ignore;
|
||||||
var json = file.GetAsText();
|
|
||||||
file.Close();
|
_metricsOverlay = new MetricsOverlay();
|
||||||
|
_metricsOverlay.CustomMinimumSize = new Vector2(340, 260);
|
||||||
_currentLevel = LevelLoader.Load(json);
|
metricsCenter.AddChild(_metricsOverlay);
|
||||||
_sim = new GameSim(_currentLevel);
|
uiRoot.AddChild(metricsCenter);
|
||||||
|
|
||||||
_levelSelectScreen.Visible = false;
|
// --- Level Select Screen (full viewport) ---
|
||||||
_boardView.Visible = true;
|
_levelSelectScreen = new LevelSelectScreen();
|
||||||
|
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
|
||||||
_boardView.BuildBoard(_currentLevel);
|
uiRoot.AddChild(_levelSelectScreen);
|
||||||
_objectivePanel.Setup(_currentLevel.Demands);
|
|
||||||
_pieceStockPanel.Setup(_currentLevel.Stock);
|
// Initialize animator
|
||||||
_controlBar.UpdateForPhase(SimPhase.Edit);
|
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
|
||||||
_controlBar.ResetTurn();
|
}
|
||||||
_metricsOverlay.Hide();
|
|
||||||
_detailPanel.Hide();
|
private void ConnectSignals()
|
||||||
_eventAnimator.ClearAll();
|
{
|
||||||
|
_levelSelectScreen.LevelSelected += OnLevelSelected;
|
||||||
_levelTitle.Text = $"CHESSISTICS — {_currentLevel.Name}";
|
_pieceStockPanel.PieceSelected += OnPieceKindSelected;
|
||||||
|
_inputMapper.PlacementRequested += OnPlacementRequested;
|
||||||
// Center camera on board
|
_inputMapper.Cancelled += OnPlacementCancelled;
|
||||||
var cam = GetNode<Camera2D>("Camera2D");
|
_controlBar.PlayPressed += OnPlay;
|
||||||
cam.Position = new Vector2(
|
_controlBar.PausePressed += OnPause;
|
||||||
_currentLevel.Width * BoardView.CellSize / 2f,
|
_controlBar.StepPressed += OnStep;
|
||||||
-_currentLevel.Height * BoardView.CellSize / 2f
|
_controlBar.StopPressed += OnStop;
|
||||||
);
|
_controlBar.SpeedChanged += OnSpeedChanged;
|
||||||
|
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
|
||||||
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
_eventAnimator.VictoryReached += OnVictory;
|
||||||
}
|
_metricsOverlay.RetryPressed += OnRetry;
|
||||||
|
_metricsOverlay.NextLevelPressed += OnNextLevel;
|
||||||
// --- Edit Phase ---
|
_detailPanel.RemoveRequested += OnRemoveRequested;
|
||||||
|
_inputMapper.CellClicked += OnCellClicked;
|
||||||
private void OnPieceKindSelected(int kindIndex)
|
}
|
||||||
{
|
|
||||||
_inputMapper.SelectPieceKind((PieceKind)kindIndex);
|
private void OnCellClicked(int col, int row)
|
||||||
}
|
{
|
||||||
|
if (_sim == null) return;
|
||||||
private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow)
|
var snap = _sim.GetSnapshot();
|
||||||
{
|
if (snap.Phase != SimPhase.Edit) return;
|
||||||
if (_sim == null) return;
|
|
||||||
|
var coords = new Coords(col, row);
|
||||||
var kind = (PieceKind)kindIndex;
|
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
|
||||||
var start = new Coords(startCol, startRow);
|
if (piece != null)
|
||||||
var end = new Coords(endCol, endRow);
|
_detailPanel.ShowPiece(piece);
|
||||||
|
else
|
||||||
var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end));
|
_detailPanel.Hide();
|
||||||
HandleEditEvents(events);
|
}
|
||||||
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
|
||||||
}
|
// --- Level Management ---
|
||||||
|
|
||||||
private void OnPlacementCancelled()
|
private void ShowLevelSelect()
|
||||||
{
|
{
|
||||||
_pieceStockPanel.ClearSelection();
|
_levelSelectScreen.Visible = true;
|
||||||
}
|
_boardView.Visible = false;
|
||||||
|
_sidePanel.Visible = false;
|
||||||
private void OnRemoveRequested(int pieceId)
|
_controlBarWrapper.Visible = false;
|
||||||
{
|
_levelTitle.Visible = false;
|
||||||
if (_sim == null) return;
|
}
|
||||||
|
|
||||||
var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId));
|
private void OnLevelSelected(int levelIndex)
|
||||||
HandleEditEvents(events);
|
{
|
||||||
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
_currentLevelIndex = levelIndex;
|
||||||
}
|
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]}";
|
||||||
case PiecePlacedEvent placed:
|
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read);
|
||||||
CreatePieceVisual(placed);
|
if (file == null)
|
||||||
UpdateStockFromSnapshot();
|
{
|
||||||
break;
|
GD.PrintErr($"Cannot open level file: {path}");
|
||||||
|
return;
|
||||||
case PieceRemovedEvent removed:
|
}
|
||||||
_eventAnimator.UnregisterPiece(removed.PieceId);
|
|
||||||
UpdateStockFromSnapshot();
|
var json = file.GetAsText();
|
||||||
_detailPanel.Hide();
|
file.Close();
|
||||||
break;
|
|
||||||
|
_currentLevel = LevelLoader.Load(json);
|
||||||
case PlacementRejectedEvent rejected:
|
_sim = new GameSim(_currentLevel);
|
||||||
GD.Print($"Placement rejected: {rejected.Reason}");
|
|
||||||
break;
|
_levelSelectScreen.Visible = false;
|
||||||
|
_boardView.Visible = true;
|
||||||
case CommandRejectedEvent rejected:
|
_sidePanel.Visible = true;
|
||||||
GD.Print($"Command rejected: {rejected.Reason}");
|
_controlBarWrapper.Visible = true;
|
||||||
break;
|
_levelTitle.Visible = true;
|
||||||
}
|
|
||||||
}
|
_boardView.BuildBoard(_currentLevel);
|
||||||
}
|
_objectivePanel.Setup(_currentLevel.Demands);
|
||||||
|
_pieceStockPanel.Setup(_currentLevel.Stock);
|
||||||
private void CreatePieceVisual(PiecePlacedEvent placed)
|
_controlBar.UpdateForPhase(SimPhase.Edit);
|
||||||
{
|
_controlBar.ResetTurn();
|
||||||
var pieceView = new PieceView();
|
_metricsOverlay.Hide();
|
||||||
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
|
_detailPanel.Hide();
|
||||||
_boardView.AddChild(pieceView);
|
_eventAnimator.ClearAll();
|
||||||
|
|
||||||
var color = placed.Kind switch
|
_levelTitle.Text = $"CHESSISTICS — {_currentLevel.Name}";
|
||||||
{
|
|
||||||
PieceKind.Rook => new Color("#4A7AB5"),
|
// Center camera on board
|
||||||
PieceKind.Bishop => new Color("#B54A8E"),
|
// Board X spans 0..W*Cell, Y spans -(H-1)*Cell .. Cell
|
||||||
PieceKind.Knight => new Color("#B5824A"),
|
_camera.Position = new Vector2(
|
||||||
_ => Colors.White
|
_currentLevel.Width * BoardView.CellSize / 2f,
|
||||||
};
|
-_currentLevel.Height * BoardView.CellSize / 2f + BoardView.CellSize
|
||||||
|
);
|
||||||
var trajectView = new TrajectView();
|
_camera.Offset = new Vector2(-SidePanelWidth / 2f, -ControlBarHeight / 2f);
|
||||||
trajectView.Setup(placed.PieceId,
|
|
||||||
_boardView.CoordsToPixel(placed.Start),
|
var snapshot = _sim.GetSnapshot();
|
||||||
_boardView.CoordsToPixel(placed.End),
|
GD.Print($"[Main] LoadLevel — snapshot null? {snapshot == null}, width={snapshot?.Width}, height={snapshot?.Height}");
|
||||||
color);
|
_inputMapper.SetSnapshot(snapshot);
|
||||||
_boardView.AddChild(trajectView);
|
}
|
||||||
|
|
||||||
_eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView);
|
// --- Edit Phase ---
|
||||||
}
|
|
||||||
|
private void OnPieceKindSelected(int kindIndex)
|
||||||
private void UpdateStockFromSnapshot()
|
{
|
||||||
{
|
_inputMapper.SelectPieceKind((PieceKind)kindIndex);
|
||||||
if (_sim == null) return;
|
}
|
||||||
var snap = _sim.GetSnapshot();
|
|
||||||
foreach (var (kind, remaining) in snap.RemainingStock)
|
private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow)
|
||||||
_pieceStockPanel.UpdateCount(kind, remaining);
|
{
|
||||||
}
|
if (_sim == null) return;
|
||||||
|
|
||||||
// --- Exec Phase ---
|
var kind = (PieceKind)kindIndex;
|
||||||
|
var start = new Coords(startCol, startRow);
|
||||||
private void OnPlay()
|
var end = new Coords(endCol, endRow);
|
||||||
{
|
|
||||||
if (_sim == null) return;
|
var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end));
|
||||||
|
HandleEditEvents(events);
|
||||||
var snap = _sim.GetSnapshot();
|
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
||||||
if (snap.Phase == SimPhase.Edit)
|
}
|
||||||
{
|
|
||||||
var events = _sim.ProcessCommand(new StartSimulationCommand());
|
private void OnPlacementCancelled()
|
||||||
foreach (var evt in events)
|
{
|
||||||
{
|
_pieceStockPanel.ClearSelection();
|
||||||
if (evt is CommandRejectedEvent r)
|
}
|
||||||
{
|
|
||||||
GD.Print($"Cannot start: {r.Reason}");
|
private void OnRemoveRequested(int pieceId)
|
||||||
return;
|
{
|
||||||
}
|
if (_sim == null) return;
|
||||||
}
|
|
||||||
}
|
var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId));
|
||||||
else if (snap.Phase == SimPhase.Paused)
|
HandleEditEvents(events);
|
||||||
{
|
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
||||||
_sim.ProcessCommand(new ResumeSimulationCommand());
|
}
|
||||||
}
|
|
||||||
|
private void HandleEditEvents(IReadOnlyList<IWorldEvent> events)
|
||||||
_running = true;
|
{
|
||||||
_controlBar.UpdateForPhase(SimPhase.Running);
|
foreach (var evt in events)
|
||||||
_simTimer.WaitTime = _simInterval;
|
{
|
||||||
_simTimer.Start();
|
switch (evt)
|
||||||
}
|
{
|
||||||
|
case PiecePlacedEvent placed:
|
||||||
private void OnPause()
|
CreatePieceVisual(placed);
|
||||||
{
|
UpdateStockFromSnapshot();
|
||||||
if (_sim == null) return;
|
break;
|
||||||
_sim.ProcessCommand(new PauseSimulationCommand());
|
|
||||||
_running = false;
|
case PieceRemovedEvent removed:
|
||||||
_simTimer.Stop();
|
_eventAnimator.UnregisterPiece(removed.PieceId);
|
||||||
_controlBar.UpdateForPhase(SimPhase.Paused);
|
UpdateStockFromSnapshot();
|
||||||
}
|
_detailPanel.Hide();
|
||||||
|
break;
|
||||||
private void OnStep()
|
|
||||||
{
|
case PlacementRejectedEvent rejected:
|
||||||
if (_sim == null || _eventAnimator.IsAnimating) return;
|
GD.Print($"Placement rejected: {rejected.Reason}");
|
||||||
var events = _sim.ProcessCommand(new StepSimulationCommand());
|
break;
|
||||||
_eventAnimator.ProcessEvents(events);
|
|
||||||
_controlBar.UpdateForPhase(_sim.GetSnapshot().Phase);
|
case CommandRejectedEvent rejected:
|
||||||
}
|
GD.Print($"Command rejected: {rejected.Reason}");
|
||||||
|
break;
|
||||||
private void OnStop()
|
}
|
||||||
{
|
}
|
||||||
if (_sim == null) return;
|
}
|
||||||
_running = false;
|
|
||||||
_simTimer.Stop();
|
private void CreatePieceVisual(PiecePlacedEvent placed)
|
||||||
_sim.ProcessCommand(new StopSimulationCommand());
|
{
|
||||||
_eventAnimator.ResetPiecePositions(_sim.GetSnapshot());
|
var pieceView = new PieceView();
|
||||||
_controlBar.UpdateForPhase(SimPhase.Edit);
|
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
|
||||||
_controlBar.ResetTurn();
|
_boardView.AddChild(pieceView);
|
||||||
_metricsOverlay.Hide();
|
|
||||||
_inputMapper.SetSnapshot(_sim.GetSnapshot());
|
var color = placed.Kind switch
|
||||||
|
{
|
||||||
// Reset objective panel
|
PieceKind.Rook => new Color("#4A7AB5"),
|
||||||
if (_currentLevel != null)
|
PieceKind.Bishop => new Color("#B54A8E"),
|
||||||
_objectivePanel.Setup(_currentLevel.Demands);
|
PieceKind.Knight => new Color("#B5824A"),
|
||||||
}
|
_ => Colors.White
|
||||||
|
};
|
||||||
private void OnSpeedChanged(float interval)
|
|
||||||
{
|
var trajectView = new TrajectView();
|
||||||
_simInterval = interval;
|
trajectView.Setup(placed.PieceId,
|
||||||
if (_simTimer.TimeLeft > 0)
|
_boardView.CoordsToPixel(placed.Start),
|
||||||
_simTimer.WaitTime = interval;
|
_boardView.CoordsToPixel(placed.End),
|
||||||
}
|
color);
|
||||||
|
_boardView.AddChild(trajectView);
|
||||||
private void OnSimTimerTick()
|
|
||||||
{
|
_eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView);
|
||||||
if (_sim == null || _eventAnimator.IsAnimating) return;
|
}
|
||||||
|
|
||||||
var events = _sim.ProcessCommand(new StepSimulationCommand());
|
private void UpdateStockFromSnapshot()
|
||||||
_eventAnimator.ProcessEvents(events);
|
{
|
||||||
}
|
if (_sim == null) return;
|
||||||
|
var snap = _sim.GetSnapshot();
|
||||||
private void OnTurnAnimationCompleted()
|
foreach (var (kind, remaining) in snap.RemainingStock)
|
||||||
{
|
_pieceStockPanel.UpdateCount(kind, remaining);
|
||||||
if (_sim == null) return;
|
}
|
||||||
var phase = _sim.GetSnapshot().Phase;
|
|
||||||
_controlBar.UpdateForPhase(phase);
|
// --- Exec Phase ---
|
||||||
|
|
||||||
if (phase == SimPhase.Victory || phase == SimPhase.Defeat || phase == SimPhase.Collision)
|
private void OnPlay()
|
||||||
{
|
{
|
||||||
_running = false;
|
if (_sim == null) return;
|
||||||
_simTimer.Stop();
|
|
||||||
}
|
var snap = _sim.GetSnapshot();
|
||||||
}
|
if (snap.Phase == SimPhase.Edit)
|
||||||
|
{
|
||||||
private void OnVictory()
|
var events = _sim.ProcessCommand(new StartSimulationCommand());
|
||||||
{
|
foreach (var evt in events)
|
||||||
_running = false;
|
{
|
||||||
_simTimer.Stop();
|
if (evt is CommandRejectedEvent r)
|
||||||
}
|
{
|
||||||
|
GD.Print($"Cannot start: {r.Reason}");
|
||||||
private void OnCollision()
|
return;
|
||||||
{
|
}
|
||||||
_running = false;
|
}
|
||||||
_simTimer.Stop();
|
}
|
||||||
_controlBar.UpdateForPhase(SimPhase.Collision);
|
else if (snap.Phase == SimPhase.Paused)
|
||||||
}
|
{
|
||||||
|
_sim.ProcessCommand(new ResumeSimulationCommand());
|
||||||
// --- Navigation ---
|
}
|
||||||
|
|
||||||
private void OnRetry()
|
_running = true;
|
||||||
{
|
_controlBar.UpdateForPhase(SimPhase.Running);
|
||||||
LoadLevel(_currentLevelIndex);
|
_simTimer.WaitTime = _simInterval;
|
||||||
}
|
_simTimer.Start();
|
||||||
|
}
|
||||||
private void OnNextLevel()
|
|
||||||
{
|
private void OnPause()
|
||||||
if (_currentLevelIndex + 1 < LevelFiles.Length)
|
{
|
||||||
LoadLevel(_currentLevelIndex + 1);
|
if (_sim == null) return;
|
||||||
else
|
_sim.ProcessCommand(new PauseSimulationCommand());
|
||||||
ShowLevelSelect();
|
_running = false;
|
||||||
}
|
_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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Scripts/Main.cs.uid
Normal file
1
Scripts/Main.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dygonjc0xhp15
|
||||||
|
|
@ -15,6 +15,7 @@ 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");
|
||||||
|
|
@ -32,6 +33,7 @@ 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,
|
||||||
|
|
@ -57,6 +59,7 @@ 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",
|
||||||
|
|
@ -64,7 +67,8 @@ 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);
|
||||||
|
|
@ -75,7 +79,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
Scripts/Pieces/PieceView.cs.uid
Normal file
1
Scripts/Pieces/PieceView.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cqnkhh3lwxrcg
|
||||||
1
Scripts/Pieces/TrajectView.cs.uid
Normal file
1
Scripts/Pieces/TrajectView.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d4mdddae1m0ia
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
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;
|
||||||
|
|
@ -22,12 +23,19 @@ 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)
|
||||||
|
|
@ -64,54 +72,39 @@ 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:
|
||||||
if (_pieceViews.TryGetValue(moved.PieceId, out var pv))
|
moveEvents.Add(moved);
|
||||||
{
|
|
||||||
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 CollisionDetectedEvent collision:
|
case PieceDestroyedEvent destroyed:
|
||||||
tween.TweenCallback(Callable.From(() =>
|
collisionEvents.Add(destroyed);
|
||||||
{
|
|
||||||
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);
|
||||||
|
|
@ -119,16 +112,17 @@ public partial class EventAnimator : Node
|
||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DeadlineExpiredEvent:
|
case TurnEndedEvent:
|
||||||
tween.TweenCallback(Callable.From(() =>
|
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
|
||||||
EmitSignal(SignalName.CollisionOccurred))); // reuse for pause
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TurnEndedEvent:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlushPhases(tween, produceEvents, transferEvents, moveEvents, collisionEvents);
|
||||||
|
|
||||||
tween.TweenCallback(Callable.From(() =>
|
tween.TweenCallback(Callable.From(() =>
|
||||||
{
|
{
|
||||||
_animating = false;
|
_animating = false;
|
||||||
|
|
@ -136,6 +130,141 @@ 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;
|
||||||
|
|
|
||||||
1
Scripts/Presentation/EventAnimator.cs.uid
Normal file
1
Scripts/Presentation/EventAnimator.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cq8o11cyd42nr
|
||||||
1
Scripts/UI/ControlBar.cs.uid
Normal file
1
Scripts/UI/ControlBar.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://0lb2jejsmsyu
|
||||||
1
Scripts/UI/DetailPanel.cs.uid
Normal file
1
Scripts/UI/DetailPanel.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b7pd55grwrmb7
|
||||||
|
|
@ -12,99 +12,210 @@ 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()
|
||||||
{
|
{
|
||||||
var panel = new PanelContainer();
|
// Full-screen dark background
|
||||||
panel.SetAnchorsPreset(LayoutPreset.FullRect);
|
var bg = new PanelContainer();
|
||||||
|
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.AddThemeConstantOverride("margin_left", 60);
|
margin.SetAnchorsPreset(LayoutPreset.FullRect);
|
||||||
margin.AddThemeConstantOverride("margin_right", 60);
|
margin.AddThemeConstantOverride("margin_left", 80);
|
||||||
|
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 vbox = new VBoxContainer();
|
var outerVBox = 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", 32);
|
title.AddThemeFontSizeOverride("font_size", 48);
|
||||||
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
|
title.AddThemeColorOverride("font_color", new Color("#FFD700"));
|
||||||
vbox.AddChild(title);
|
headerBox.AddChild(title);
|
||||||
|
|
||||||
var subtitle = new Label
|
var subtitle = new Label
|
||||||
{
|
{
|
||||||
Text = "Prototype — Selectionnez un niveau",
|
Text = "Selectionnez un niveau",
|
||||||
HorizontalAlignment = HorizontalAlignment.Center
|
HorizontalAlignment = HorizontalAlignment.Center
|
||||||
};
|
};
|
||||||
subtitle.AddThemeFontSizeOverride("font_size", 14);
|
subtitle.AddThemeFontSizeOverride("font_size", 15);
|
||||||
subtitle.AddThemeColorOverride("font_color", new Color("#AAAAAA"));
|
subtitle.AddThemeColorOverride("font_color", new Color("#777777"));
|
||||||
vbox.AddChild(subtitle);
|
headerBox.AddChild(subtitle);
|
||||||
|
|
||||||
vbox.AddChild(new HSeparator());
|
outerVBox.AddChild(headerBox);
|
||||||
|
|
||||||
var grid = new HBoxContainer();
|
// Spacer
|
||||||
grid.Alignment = BoxContainer.AlignmentMode.Center;
|
outerVBox.AddChild(new Control { CustomMinimumSize = new Vector2(0, 48) });
|
||||||
|
|
||||||
|
// --- 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];
|
||||||
var card = CreateLevelCard(i, name, desc);
|
grid.AddChild(CreateLevelCard(i, name, desc));
|
||||||
grid.AddChild(card);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vbox.AddChild(grid);
|
scroll.AddChild(grid);
|
||||||
margin.AddChild(vbox);
|
outerVBox.AddChild(scroll);
|
||||||
panel.AddChild(margin);
|
|
||||||
AddChild(panel);
|
margin.AddChild(outerVBox);
|
||||||
|
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(220, 160)
|
CustomMinimumSize = new Vector2(300, 240),
|
||||||
|
SizeFlagsVertical = SizeFlags.ShrinkCenter
|
||||||
};
|
};
|
||||||
|
|
||||||
var vbox = new VBoxContainer();
|
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();
|
||||||
|
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("#AAAAAA"));
|
numLabel.AddThemeColorOverride("font_color", new Color("#666666"));
|
||||||
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", 18);
|
nameLabel.AddThemeFontSizeOverride("font_size", 22);
|
||||||
|
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", 11);
|
descLabel.AddThemeFontSizeOverride("font_size", 13);
|
||||||
descLabel.AddThemeColorOverride("font_color", new Color("#CCCCCC"));
|
descLabel.AddThemeColorOverride("font_color", new Color("#999999"));
|
||||||
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(100, 32)
|
CustomMinimumSize = new Vector2(120, 38),
|
||||||
|
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);
|
||||||
|
|
|
||||||
1
Scripts/UI/LevelSelectScreen.cs.uid
Normal file
1
Scripts/UI/LevelSelectScreen.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dhg770rwx2gop
|
||||||
1
Scripts/UI/MetricsOverlay.cs.uid
Normal file
1
Scripts/UI/MetricsOverlay.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dpi1a85m4gva8
|
||||||
|
|
@ -16,6 +16,7 @@ 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());
|
||||||
|
|
|
||||||
1
Scripts/UI/ObjectivePanel.cs.uid
Normal file
1
Scripts/UI/ObjectivePanel.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5q1qrjsbrp6m
|
||||||
|
|
@ -24,6 +24,7 @@ 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());
|
||||||
|
|
@ -92,6 +93,7 @@ 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",
|
||||||
|
|
|
||||||
1
Scripts/UI/PieceStockPanel.cs.uid
Normal file
1
Scripts/UI/PieceStockPanel.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b7vf8ury0hdts
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ci3ilv3dv7wxh
|
||||||
1
chessistics-engine/Commands/IWorldCommand.cs.uid
Normal file
1
chessistics-engine/Commands/IWorldCommand.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://qb3ybyegoh7k
|
||||||
1
chessistics-engine/Commands/WorldCommand.cs.uid
Normal file
1
chessistics-engine/Commands/WorldCommand.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dhrp4e6pkpkym
|
||||||
|
|
@ -10,12 +10,14 @@ 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)
|
public PlacePieceCommand(PieceKind kind, Coords start, Coords end, int level = 1)
|
||||||
{
|
{
|
||||||
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)
|
||||||
|
|
@ -44,7 +46,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);
|
state.NextPieceId++, Kind, Start, End, state.Pieces.Count, Level);
|
||||||
|
|
||||||
piece.CargoFilter = InferCargoFilter(state, piece);
|
piece.CargoFilter = InferCargoFilter(state, piece);
|
||||||
|
|
||||||
|
|
@ -189,7 +191,7 @@ public class StepSimulationCommand : WorldCommand
|
||||||
|
|
||||||
TurnExecutor.ExecuteTurn(state, changeList);
|
TurnExecutor.ExecuteTurn(state, changeList);
|
||||||
|
|
||||||
// After a step, remain in Paused unless victory/defeat/collision occurred
|
// After a step, remain in Paused unless victory/defeat occurred
|
||||||
if (state.Phase == SimPhase.Running)
|
if (state.Phase == SimPhase.Running)
|
||||||
state.Phase = SimPhase.Paused;
|
state.Phase = SimPhase.Paused;
|
||||||
}
|
}
|
||||||
|
|
@ -206,6 +208,10 @@ 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;
|
||||||
|
|
@ -213,7 +219,7 @@ public class StopSimulationCommand : WorldCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var pos in state.ProductionBuffers.Keys.ToList())
|
foreach (var pos in state.ProductionBuffers.Keys.ToList())
|
||||||
state.ProductionBuffers[pos] = null;
|
state.ProductionBuffers[pos] = 0;
|
||||||
|
|
||||||
foreach (var demand in state.Demands.Values)
|
foreach (var demand in state.Demands.Values)
|
||||||
demand.ReceivedCount = 0;
|
demand.ReceivedCount = 0;
|
||||||
|
|
|
||||||
1
chessistics-engine/Commands/WorldCommands.cs.uid
Normal file
1
chessistics-engine/Commands/WorldCommands.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://blxykw7srkq2x
|
||||||
1
chessistics-engine/Events/IWorldEvent.cs.uid
Normal file
1
chessistics-engine/Events/IWorldEvent.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cguse2y3ma1on
|
||||||
|
|
@ -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
|
// Turn events — all carry TurnNumber for animation grouping
|
||||||
public record TurnStartedEvent(int TurnNumber) : IWorldEvent;
|
public record TurnStartedEvent(int TurnNumber) : IWorldEvent;
|
||||||
public record PieceMovedEvent(int PieceId, Coords From, Coords To) : IWorldEvent;
|
public record PieceMovedEvent(int TurnNumber, int PieceId, Coords From, Coords To) : IWorldEvent;
|
||||||
public record CollisionDetectedEvent(int PieceIdA, int PieceIdB, Coords Cell) : IWorldEvent;
|
public record PieceDestroyedEvent(int TurnNumber, int PieceId, int? DestroyerPieceId, Coords Cell) : IWorldEvent;
|
||||||
public record CargoTransferredEvent(Coords From, Coords To, CargoType Type, int? GivingPieceId, int? ReceivingPieceId) : IWorldEvent;
|
public record CargoTransferredEvent(int TurnNumber, Coords From, Coords To, CargoType Type, int? GivingPieceId, int? ReceivingPieceId) : IWorldEvent;
|
||||||
public record CargoProducedEvent(Coords ProductionCell, CargoType Type) : IWorldEvent;
|
public record CargoProducedEvent(int TurnNumber, Coords ProductionCell, CargoType Type) : IWorldEvent;
|
||||||
public record DemandProgressEvent(Coords DemandCell, string Name, int Current, int Required) : IWorldEvent;
|
public record DemandProgressEvent(int TurnNumber, Coords DemandCell, string Name, int Current, int Required) : IWorldEvent;
|
||||||
public record VictoryEvent(Metrics Metrics) : IWorldEvent;
|
public record VictoryEvent(int TurnNumber, Metrics Metrics) : IWorldEvent;
|
||||||
public record DeadlineExpiredEvent(Coords DemandCell, string Name) : IWorldEvent;
|
public record DeadlineExpiredEvent(int TurnNumber, Coords DemandCell, string Name) : IWorldEvent;
|
||||||
public record TurnEndedEvent(int TurnNumber) : IWorldEvent;
|
public record TurnEndedEvent(int TurnNumber) : IWorldEvent;
|
||||||
|
|
|
||||||
1
chessistics-engine/Events/WorldEvents.cs.uid
Normal file
1
chessistics-engine/Events/WorldEvents.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cq5huqxbjpw2a
|
||||||
|
|
@ -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.Interval
|
new Coords(p.Col, p.Row), p.Name, ParseCargo(p.Cargo), p.Amount
|
||||||
)).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)).ToList()
|
Stock = dto.Stock.Select(s => new PieceStock(ParseKind(s.Kind), s.Count, s.Level)).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +52,7 @@ 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,
|
||||||
|
|
@ -92,7 +93,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 Interval { get; set; }
|
public int Amount { get; set; } = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DemandDto
|
private class DemandDto
|
||||||
|
|
@ -115,5 +116,6 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
chessistics-engine/Loading/LevelLoader.cs.uid
Normal file
1
chessistics-engine/Loading/LevelLoader.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bbajsqcri0wa1
|
||||||
|
|
@ -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.Interval, state.ProductionBuffers[p.Position]))
|
.Select(p => new ProductionSnapshot(p.Position, p.Name, p.Cargo, p.Amount, 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.StartCell, p.EndCell, p.CurrentCell, p.Cargo, p.CargoFilter, p.SocialStatus))
|
.Select(p => new PieceSnapshot(p.Id, p.Kind, p.Level, 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 Interval, CargoType? Buffer);
|
public record ProductionSnapshot(Coords Position, string Name, CargoType Cargo, int Amount, int BufferCount);
|
||||||
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, Coords StartCell, Coords EndCell, Coords CurrentCell, CargoType? Cargo, CargoType? CargoFilter, int SocialStatus);
|
public record PieceSnapshot(int Id, PieceKind Kind, int Level, Coords StartCell, Coords EndCell, Coords CurrentCell, CargoType? Cargo, CargoType? CargoFilter, int SocialStatus);
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/BoardSnapshot.cs.uid
Normal file
1
chessistics-engine/Model/BoardSnapshot.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cnpyeaslw4mnb
|
||||||
|
|
@ -10,7 +10,8 @@ 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 Dictionary<Coords, CargoType?> ProductionBuffers { get; }
|
public List<PieceState> DestroyedPieces { get; } = new();
|
||||||
|
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; }
|
||||||
|
|
@ -34,7 +35,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, CargoType?>();
|
ProductionBuffers = new Dictionary<Coords, int>();
|
||||||
RemainingStock = new Dictionary<PieceKind, int>();
|
RemainingStock = new Dictionary<PieceKind, int>();
|
||||||
OccupiedCells = new HashSet<Coords>();
|
OccupiedCells = new HashSet<Coords>();
|
||||||
|
|
||||||
|
|
@ -56,7 +57,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] = null;
|
ProductionBuffers[prod.Position] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place demands
|
// Place demands
|
||||||
|
|
@ -119,6 +120,7 @@ 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();
|
||||||
|
|
@ -140,7 +142,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] = null;
|
ProductionBuffers[prod.Position] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var demand in _levelDef.Demands)
|
foreach (var demand in _levelDef.Demands)
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/BoardState.cs.uid
Normal file
1
chessistics-engine/Model/BoardState.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dq6vosmhy6ofr
|
||||||
1
chessistics-engine/Model/CargoType.cs.uid
Normal file
1
chessistics-engine/Model/CargoType.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dmuxaq4gvi3v1
|
||||||
1
chessistics-engine/Model/CellType.cs.uid
Normal file
1
chessistics-engine/Model/CellType.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dblei0vt3ulge
|
||||||
1
chessistics-engine/Model/Coords.cs.uid
Normal file
1
chessistics-engine/Model/Coords.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cr4rqwl666m6i
|
||||||
1
chessistics-engine/Model/DemandDef.cs.uid
Normal file
1
chessistics-engine/Model/DemandDef.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://iv2oeeyht17f
|
||||||
1
chessistics-engine/Model/DemandState.cs.uid
Normal file
1
chessistics-engine/Model/DemandState.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dwakj4uour8j0
|
||||||
1
chessistics-engine/Model/LevelDef.cs.uid
Normal file
1
chessistics-engine/Model/LevelDef.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dxfk6kjtkl5th
|
||||||
1
chessistics-engine/Model/Metrics.cs.uid
Normal file
1
chessistics-engine/Model/Metrics.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://boa1klorcvwn7
|
||||||
|
|
@ -2,6 +2,7 @@ namespace Chessistics.Engine.Model;
|
||||||
|
|
||||||
public enum PieceKind
|
public enum PieceKind
|
||||||
{
|
{
|
||||||
|
Pawn,
|
||||||
Rook,
|
Rook,
|
||||||
Bishop,
|
Bishop,
|
||||||
Knight
|
Knight
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/PieceKind.cs.uid
Normal file
1
chessistics-engine/Model/PieceKind.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b7ayb8mmmmvos
|
||||||
|
|
@ -4,6 +4,7 @@ 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,
|
||||||
|
|
@ -12,6 +13,7 @@ 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
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/PieceRules.cs.uid
Normal file
1
chessistics-engine/Model/PieceRules.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://g4se3fjdvw40
|
||||||
|
|
@ -4,6 +4,7 @@ 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; }
|
||||||
|
|
@ -12,10 +13,11 @@ 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)
|
public PieceState(int id, PieceKind kind, Coords startCell, Coords endCell, int placementOrder, int level = 1)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Kind = kind;
|
Kind = kind;
|
||||||
|
Level = level;
|
||||||
StartCell = startCell;
|
StartCell = startCell;
|
||||||
EndCell = endCell;
|
EndCell = endCell;
|
||||||
CurrentCell = startCell;
|
CurrentCell = startCell;
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/PieceState.cs.uid
Normal file
1
chessistics-engine/Model/PieceState.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cqcbtrhwqx1oq
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
namespace Chessistics.Engine.Model;
|
namespace Chessistics.Engine.Model;
|
||||||
|
|
||||||
public record PieceStock(PieceKind Kind, int Count);
|
public record PieceStock(PieceKind Kind, int Count, int Level = 1);
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/PieceStock.cs.uid
Normal file
1
chessistics-engine/Model/PieceStock.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://iyhkgjgadvbq
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
namespace Chessistics.Engine.Model;
|
namespace Chessistics.Engine.Model;
|
||||||
|
|
||||||
public record ProductionDef(Coords Position, string Name, CargoType Cargo, int Interval);
|
public record ProductionDef(Coords Position, string Name, CargoType Cargo, int Amount = 1);
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/ProductionDef.cs.uid
Normal file
1
chessistics-engine/Model/ProductionDef.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://3jsqpr5wfblc
|
||||||
|
|
@ -5,7 +5,6 @@ public enum SimPhase
|
||||||
Edit,
|
Edit,
|
||||||
Running,
|
Running,
|
||||||
Paused,
|
Paused,
|
||||||
Collision,
|
|
||||||
Victory,
|
Victory,
|
||||||
Defeat
|
Defeat
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
chessistics-engine/Model/SimPhase.cs.uid
Normal file
1
chessistics-engine/Model/SimPhase.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cmspgsp8mcvtd
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
55
chessistics-engine/Rules/CollisionResolver.cs
Normal file
55
chessistics-engine/Rules/CollisionResolver.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
chessistics-engine/Rules/CollisionResolver.cs.uid
Normal file
1
chessistics-engine/Rules/CollisionResolver.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ddl7yl8k7h4qp
|
||||||
|
|
@ -19,6 +19,7 @@ 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),
|
||||||
|
|
|
||||||
1
chessistics-engine/Rules/MoveValidator.cs.uid
Normal file
1
chessistics-engine/Rules/MoveValidator.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://danfmpxdyyc3w
|
||||||
|
|
@ -26,29 +26,33 @@ 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] != null)
|
.Where(p => state.ProductionBuffers[p.Position] > 0)
|
||||||
.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 = state.ProductionBuffers[prod.Position]!.Value;
|
var cargoType = prod.Cargo;
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
if (receivers.Count == 0) continue;
|
foreach (var receiver in receivers)
|
||||||
|
{
|
||||||
|
if (state.ProductionBuffers[prod.Position] <= 0) break;
|
||||||
|
|
||||||
var receiver = receivers[0];
|
receiver.Cargo = cargoType;
|
||||||
receiver.Cargo = cargoType;
|
state.ProductionBuffers[prod.Position]--;
|
||||||
state.ProductionBuffers[prod.Position] = null;
|
participated.Add(receiver.Id);
|
||||||
participated.Add(receiver.Id);
|
|
||||||
productionGave.Add(prod.Position);
|
|
||||||
|
|
||||||
events.Add(new CargoTransferredEvent(
|
events.Add(new CargoTransferredEvent(
|
||||||
prod.Position, receiver.CurrentCell, cargoType,
|
state.TurnNumber, 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,8 +63,7 @@ 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)
|
||||||
.ThenBy(p => MinDistanceToProduction(p.CurrentCell, state, p.Cargo))
|
.ThenByDescending(p => p.Level)
|
||||||
.ThenBy(p => p.PlacementOrder)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var giver in givers)
|
foreach (var giver in givers)
|
||||||
|
|
@ -69,7 +72,7 @@ public static class TransferResolver
|
||||||
|
|
||||||
var cargoType = giver.Cargo!.Value;
|
var cargoType = giver.Cargo!.Value;
|
||||||
|
|
||||||
// Priority 1: deliver to adjacent demand
|
// Priority 1: deliver to adjacent demand (always accepts matching cargo, even when satisfied)
|
||||||
var adjacentDemand = GetAdjacentCompatibleDemand(state, giver.CurrentCell, cargoType);
|
var adjacentDemand = GetAdjacentCompatibleDemand(state, giver.CurrentCell, cargoType);
|
||||||
if (adjacentDemand != null)
|
if (adjacentDemand != null)
|
||||||
{
|
{
|
||||||
|
|
@ -78,20 +81,19 @@ public static class TransferResolver
|
||||||
participated.Add(giver.Id);
|
participated.Add(giver.Id);
|
||||||
|
|
||||||
events.Add(new CargoTransferredEvent(
|
events.Add(new CargoTransferredEvent(
|
||||||
giver.CurrentCell, adjacentDemand.Position, cargoType,
|
state.TurnNumber, giver.CurrentCell, adjacentDemand.Position, cargoType,
|
||||||
GivingPieceId: giver.Id, ReceivingPieceId: null));
|
GivingPieceId: giver.Id, ReceivingPieceId: null));
|
||||||
|
|
||||||
events.Add(new DemandProgressEvent(
|
events.Add(new DemandProgressEvent(
|
||||||
adjacentDemand.Position, adjacentDemand.Name,
|
state.TurnNumber, 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,
|
||||||
forwardDirection: true, cargoType: cargoType);
|
cargoType: cargoType);
|
||||||
if (receivers.Count == 0) continue;
|
if (receivers.Count == 0) continue;
|
||||||
|
|
||||||
var receiver = receivers[0];
|
var receiver = receivers[0];
|
||||||
|
|
@ -101,32 +103,26 @@ public static class TransferResolver
|
||||||
participated.Add(receiver.Id);
|
participated.Add(receiver.Id);
|
||||||
|
|
||||||
events.Add(new CargoTransferredEvent(
|
events.Add(new CargoTransferredEvent(
|
||||||
giver.CurrentCell, receiver.CurrentCell, cargoType,
|
state.TurnNumber, 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,
|
||||||
bool forwardDirection = false, CargoType? cargoType = null)
|
CargoType? cargoType = null)
|
||||||
{
|
{
|
||||||
var adjacent = position.GetAdjacent4(state.Width, state.Height);
|
var adjacent = position.GetAdjacent4(state.Width, state.Height);
|
||||||
|
|
||||||
var query = state.Pieces
|
return 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)
|
||||||
// For piece-to-piece transfers, prefer receivers farther from production
|
.ThenBy(p => ClockwiseOrder(p.CurrentCell, position, state.TurnNumber))
|
||||||
// (pushes cargo forward through relay chains instead of backward).
|
.ToList();
|
||||||
// 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(
|
||||||
|
|
@ -135,20 +131,35 @@ 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.IsSatisfied
|
.Where(d => d.Cargo == cargoType
|
||||||
&& d.Cargo == cargoType
|
|
||||||
&& adjacent.Contains(d.Position))
|
&& adjacent.Contains(d.Position))
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int MinDistanceToProduction(Coords cell, BoardState state, CargoType? cargoType = null)
|
/// <summary>
|
||||||
|
/// 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)
|
||||||
{
|
{
|
||||||
var productions = cargoType != null
|
int dx = pieceCell.Col - center.Col;
|
||||||
? state.Productions.Where(kv => kv.Value.Cargo == cargoType).Select(kv => kv.Key)
|
int dy = pieceCell.Row - center.Row;
|
||||||
: state.Productions.Keys;
|
|
||||||
|
|
||||||
var prodList = productions.ToList();
|
int baseOrder = (dx, dy) switch
|
||||||
if (prodList.Count == 0) return int.MaxValue;
|
{
|
||||||
return prodList.Min(p => cell.ManhattanDistance(p));
|
(1, 0) => 0, // right
|
||||||
|
(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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
chessistics-engine/Rules/TransferResolver.cs.uid
Normal file
1
chessistics-engine/Rules/TransferResolver.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c55a21x5tw5bo
|
||||||
1
chessistics-engine/Rules/VictoryChecker.cs.uid
Normal file
1
chessistics-engine/Rules/VictoryChecker.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://uh7qhohnsxpa
|
||||||
1
chessistics-engine/Simulation/GameSim.cs.uid
Normal file
1
chessistics-engine/Simulation/GameSim.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c3aghlujtx44f
|
||||||
|
|
@ -11,39 +11,41 @@ public static class TurnExecutor
|
||||||
state.TurnNumber++;
|
state.TurnNumber++;
|
||||||
changeList.Add(new TurnStartedEvent(state.TurnNumber));
|
changeList.Add(new TurnStartedEvent(state.TurnNumber));
|
||||||
|
|
||||||
// Sub-phase 1: MOVEMENT
|
// Sub-phase 1: PRODUCTION
|
||||||
ExecuteMovement(state, changeList);
|
ExecuteProduction(state, changeList);
|
||||||
|
|
||||||
// Sub-phase 2: COLLISION DETECTION
|
// Sub-phase 2: TRANSFERS
|
||||||
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 4: PRODUCTION
|
// Sub-phase 3: MOVEMENT
|
||||||
ExecuteProduction(state, changeList);
|
ExecuteMovement(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(ComputeMetrics(state)));
|
changeList.Add(new VictoryEvent(state.TurnNumber, 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(demand.Position, demand.Name));
|
changeList.Add(new DeadlineExpiredEvent(state.TurnNumber, demand.Position, demand.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
changeList.Add(new TurnEndedEvent(state.TurnNumber));
|
changeList.Add(new TurnEndedEvent(state.TurnNumber));
|
||||||
|
|
@ -59,7 +61,7 @@ public static class TurnExecutor
|
||||||
{
|
{
|
||||||
piece.CurrentCell = to;
|
piece.CurrentCell = to;
|
||||||
state.OccupiedCells.Add(to);
|
state.OccupiedCells.Add(to);
|
||||||
changeList.Add(new PieceMovedEvent(piece.Id, from, to));
|
changeList.Add(new PieceMovedEvent(state.TurnNumber, piece.Id, from, to));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,21 +69,15 @@ public static class TurnExecutor
|
||||||
{
|
{
|
||||||
foreach (var (pos, prod) in state.Productions)
|
foreach (var (pos, prod) in state.Productions)
|
||||||
{
|
{
|
||||||
if (state.ProductionBuffers[pos] != null)
|
state.ProductionBuffers[pos] = prod.Amount;
|
||||||
continue; // buffer already full
|
changeList.Add(new CargoProducedEvent(state.TurnNumber, pos, prod.Cargo));
|
||||||
|
|
||||||
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,
|
PiecesUsed: state.Pieces.Count + state.DestroyedPieces.Count,
|
||||||
TurnsTaken: state.TurnNumber,
|
TurnsTaken: state.TurnNumber,
|
||||||
CellsOccupied: state.OccupiedCells.Count
|
CellsOccupied: state.OccupiedCells.Count
|
||||||
);
|
);
|
||||||
|
|
|
||||||
1
chessistics-engine/Simulation/TurnExecutor.cs.uid
Normal file
1
chessistics-engine/Simulation/TurnExecutor.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c1bwhv57ykh2x
|
||||||
|
|
@ -17,9 +17,9 @@ public class BoardBuilder
|
||||||
_height = height;
|
_height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BoardBuilder WithProduction(int col, int row, string name, CargoType cargo, int interval = 2)
|
public BoardBuilder WithProduction(int col, int row, string name, CargoType cargo, int amount = 1)
|
||||||
{
|
{
|
||||||
_productions.Add(new ProductionDef(new Coords(col, row), name, cargo, interval));
|
_productions.Add(new ProductionDef(new Coords(col, row), name, cargo, amount));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
chessistics-tests/Helpers/BoardBuilder.cs.uid
Normal file
1
chessistics-tests/Helpers/BoardBuilder.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bowilag4t3fw7
|
||||||
1
chessistics-tests/Helpers/SimHelper.cs.uid
Normal file
1
chessistics-tests/Helpers/SimHelper.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://b6bmm28wyg8oq
|
||||||
|
|
@ -42,8 +42,6 @@ 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);
|
||||||
|
|
|
||||||
1
chessistics-tests/Loading/LevelLoaderTests.cs.uid
Normal file
1
chessistics-tests/Loading/LevelLoaderTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c67yiwl47b1gq
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
chessistics-tests/Rules/CollisionResolverTests.cs
Normal file
87
chessistics-tests/Rules/CollisionResolverTests.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
chessistics-tests/Rules/CollisionResolverTests.cs.uid
Normal file
1
chessistics-tests/Rules/CollisionResolverTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dvjr7naqe8v28
|
||||||
|
|
@ -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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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)
|
||||||
|
|
|
||||||
1
chessistics-tests/Rules/MoveValidatorTests.cs.uid
Normal file
1
chessistics-tests/Rules/MoveValidatorTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://rmx2djjvmcoj
|
||||||
|
|
@ -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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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)] = CargoType.Wood;
|
board.ProductionBuffers[new Coords(0, 0)] = 1;
|
||||||
|
|
||||||
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.Null(board.ProductionBuffers[new Coords(0, 0)]);
|
Assert.Equal(0, 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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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)] = CargoType.Wood;
|
board.ProductionBuffers[new Coords(0, 0)] = 1;
|
||||||
|
|
||||||
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(CargoType.Wood, board.ProductionBuffers[new Coords(0, 0)]);
|
Assert.Equal(1, 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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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,41 +203,71 @@ public class TransferResolverTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TieBreaker_PlacementOrder()
|
public void TieBreaker_ClockwiseDirection_EvenTurn()
|
||||||
{
|
{
|
||||||
var board = new BoardBuilder(4, 4)
|
var board = new BoardBuilder(4, 4)
|
||||||
.WithProduction(0, 0, "P", CargoType.Wood, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
||||||
// Two knights (same status 3) with cargo, both adjacent to same empty receiver
|
// Giver at (1,1) with cargo, two receivers: right(2,1) and up(1,2)
|
||||||
var knight1 = new PieceState(1, PieceKind.Knight, new Coords(2, 0), new Coords(0, 1), 0); // earlier
|
// On even turn (TurnNumber=0), clockwise from right: right=0 < up=1
|
||||||
knight1.CurrentCell = new Coords(2, 0);
|
var giver = new PieceState(1, PieceKind.Rook, new Coords(1, 1), new Coords(2, 1), 0);
|
||||||
knight1.Cargo = CargoType.Wood;
|
giver.CurrentCell = new Coords(1, 1);
|
||||||
|
giver.Cargo = CargoType.Wood;
|
||||||
|
|
||||||
var knight2 = new PieceState(2, PieceKind.Knight, new Coords(2, 2), new Coords(0, 3), 1); // later
|
var receiverRight = new PieceState(2, PieceKind.Rook, new Coords(2, 1), new Coords(3, 1), 1);
|
||||||
knight2.CurrentCell = new Coords(2, 2);
|
receiverRight.CurrentCell = new Coords(2, 1); // right of giver
|
||||||
knight2.Cargo = CargoType.Wood;
|
|
||||||
|
|
||||||
var receiver = new PieceState(3, PieceKind.Knight, new Coords(2, 1), new Coords(0, 2), 2);
|
var receiverUp = new PieceState(3, PieceKind.Rook, new Coords(1, 2), new Coords(1, 3), 2);
|
||||||
receiver.CurrentCell = new Coords(2, 1); // adjacent to both (2,0) and (2,2)
|
receiverUp.CurrentCell = new Coords(1, 2); // up of giver
|
||||||
|
|
||||||
board.Pieces.AddRange([knight1, knight2, receiver]);
|
board.Pieces.AddRange([giver, receiverRight, receiverUp]);
|
||||||
|
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();
|
||||||
|
|
||||||
// knight1 is closer to production at (0,0): dist = 2, knight2: dist = 4
|
// On even turn, right(0) has priority over up(1)
|
||||||
// So knight1 gives first due to proximity (tiebreaker before placement order)
|
Assert.Equal(2, transfer.ReceivingPieceId); // receiverRight
|
||||||
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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
@ -269,7 +299,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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
@ -293,7 +323,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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
|
||||||
1
chessistics-tests/Rules/TransferResolverTests.cs.uid
Normal file
1
chessistics-tests/Rules/TransferResolverTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://7yqfkoottaie
|
||||||
|
|
@ -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, 2)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.WithProduction(5, 0, "Carriere", CargoType.Stone, 2)
|
.WithProduction(5, 0, "Carriere", CargoType.Stone)
|
||||||
.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, 2)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
|
||||||
1
chessistics-tests/Simulation/FullLevelTests.cs.uid
Normal file
1
chessistics-tests/Simulation/FullLevelTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bxu33tvxk3e0d
|
||||||
|
|
@ -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, 2)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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,18 +120,17 @@ public class GameSimTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Production_GeneratesOnInterval()
|
public void Production_GeneratesEveryTurn()
|
||||||
{
|
{
|
||||||
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();
|
||||||
// With interval 2, produces on turns 2, 4, 6 (buffer freed each time by adjacent piece)
|
// Production fires every turn
|
||||||
Assert.True(prodEvents.Count >= 2, $"Expected at least 2 productions, got {prodEvents.Count}");
|
Assert.Equal(6, prodEvents.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -139,7 +138,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, 1)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
@ -159,7 +158,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, 2)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
|
||||||
1
chessistics-tests/Simulation/GameSimTests.cs.uid
Normal file
1
chessistics-tests/Simulation/GameSimTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dx5nefi0k4vpb
|
||||||
|
|
@ -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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.WithProduction(0, 1, "Carriere", CargoType.Stone, 1)
|
.WithProduction(0, 1, "Carriere", CargoType.Stone)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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 CollisionDetectedEvent);
|
Assert.DoesNotContain(allEvents, e => e is PieceDestroyedEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
[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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.WithProduction(3, 0, "Carriere", CargoType.Stone, 1)
|
.WithProduction(3, 0, "Carriere", CargoType.Stone)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "Scierie", CargoType.Wood)
|
||||||
.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, 1)
|
.WithProduction(0, 0, "P", CargoType.Wood)
|
||||||
.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();
|
||||||
|
|
|
||||||
1
chessistics-tests/Simulation/SolvabilityTests.cs.uid
Normal file
1
chessistics-tests/Simulation/SolvabilityTests.cs.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://ciq3nnqbtumxg
|
||||||
|
|
@ -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 1 cargaison tous les N coups. Donne automatiquement a une piece adjacente disponible. |
|
| **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. |
|
||||||
| **Demande** | Icone cible + nom (ex: "Depot") + jauge | Recoit la cargaison d'une piece adjacente qui arrive avec un colis compatible. |
|
| **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. |
|
||||||
|
|
||||||
### 2.3 Cargaison
|
### 2.3 Cargaison
|
||||||
|
|
||||||
|
|
@ -85,7 +85,22 @@ 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
|
||||||
|
|
||||||
3 types, niveau unique :
|
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.
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
@ -130,13 +145,15 @@ 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 (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.
|
> 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).
|
||||||
|
|
||||||
### 3.3 Occupation et blocage
|
### 3.3 Occupation et collision
|
||||||
|
|
||||||
- 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)
|
||||||
- Deux pieces ne peuvent **jamais** occuper la meme case au meme coup
|
- 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**.
|
||||||
|
- 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -159,21 +176,30 @@ 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 par statut social
|
### 4.3 Priorite et departage
|
||||||
|
|
||||||
Quand plusieurs transferts sont possibles au meme point, le **statut social** determine l'ordre :
|
Quand plusieurs transferts sont possibles au meme point, la priorite determine l'ordre :
|
||||||
|
|
||||||
**Regle de don** : parmi les pieces avec colis adjacentes a un meme receveur, celle de **statut le plus eleve donne en premier**.
|
**Chaine de priorite** : statut social (desc) → niveau de piece (desc) → direction en sens horaire.
|
||||||
|
|
||||||
**Regle de reception** : parmi les pieces sans colis adjacentes a un meme donneur, celle de **statut le plus eleve recoit 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 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)
|
||||||
|
|
@ -217,30 +243,33 @@ 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, dans cet ordre :
|
A chaque coup, toutes les pieces jouent chaque etape **simultanement**, dans cet ordre :
|
||||||
|
|
||||||
```
|
```
|
||||||
1. MOUVEMENT : toutes les pieces bougent simultanement
|
1. PRODUCTION : les cases de production remplissent leur buffer
|
||||||
|
(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)
|
||||||
|
|
||||||
2. DETECTION DE COLLISION : si deux pieces sont sur la meme case → erreur
|
4. RESOLUTION DE COLLISION : si deux pieces sont sur la meme case,
|
||||||
|
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
|
||||||
|
|
||||||
Deux pieces ne peuvent pas occuper la meme case au meme coup. Si cela arrive :
|
Quand deux pieces ou plus occupent la meme case apres le mouvement :
|
||||||
- Les deux pieces clignotent en rouge
|
- La piece de **statut le plus eleve** survit, les autres sont **detruites**
|
||||||
- La simulation se met en **pause**
|
- A statut egal, le **niveau** departage (niveau superieur survit)
|
||||||
- Le joueur doit reorganiser ses pieces (revenir en mode edition)
|
- A egalite parfaite (meme statut ET meme niveau), **destruction mutuelle** (aucun survivant)
|
||||||
|
- La piece survivante conserve sa cargaison. Les cargaisons detruites sont perdues.
|
||||||
|
- La simulation **continue** (pas de pause automatique)
|
||||||
|
|
||||||
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).
|
Les collisions deviennent un element strategique — le joueur doit anticiper les croisements de trajectoires et espacer ses chaines pour les eviter.
|
||||||
|
|
||||||
### 5.3 Condition de victoire
|
### 5.3 Condition de victoire
|
||||||
|
|
||||||
|
|
@ -406,7 +435,7 @@ Le joueur peut arreter la simulation a tout moment, reorganiser, et relancer.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Les 3 niveaux du prototype
|
## 8. Les 6 niveaux du prototype
|
||||||
|
|
||||||
### Niveau 1 — "Premier Convoi"
|
### Niveau 1 — "Premier Convoi"
|
||||||
|
|
||||||
|
|
@ -559,6 +588,86 @@ 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.
|
||||||
|
|
@ -673,7 +782,13 @@ 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 = erreur stricte ? | Stricte (pause) vs tolerante | **Stricte** — le joueur voit et corrige. Plus simple a implementer. |
|
| 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. |
|
||||||
| 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 ? | Proximite > anciennete | **Proximite puis anciennete** — intuitif, pas de regle arbitraire |
|
| Egalite de statut social ? | Niveau > direction horaire | **Niveau puis direction horaire alternee** (pairs: depuis droite, impairs: depuis gauche) — deterministe, pas de biais permanent |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,14 @@ 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", "GL Compatibility")
|
config/features=PackedStringArray("4.6", "C#", "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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue