Overhaul turn mechanics, collision destruction, and visual animations
- New turn order: produce -> transfer -> move -> collision resolution - Collisions now destroy weaker pieces (status > level > mutual destruction) instead of halting the simulation. SimPhase.Collision removed. - Add piece Level property (all level 1 in proto, prepared for future) - Production fires every turn (interval concept removed), buffer = Amount (default 1, future 2-4), leftovers overwritten each turn - Transfer tiebreaker: status > level > clockwise direction (alternating even/odd turns in y-up coords), replaces distance-to-production - Demands always accept matching cargo even when already satisfied - TurnNumber added to all turn events for animation grouping - Simultaneous animations: produce flash, cargo slide, parallel piece moves - Camera centering fix + middle-click pan - GDD updated with new rules + lore section added Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd43df8820
commit
a7280b1a5a
40 changed files with 1437 additions and 853 deletions
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,13 @@
|
||||||
"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", "interval": 2 }
|
||||||
],
|
],
|
||||||
"demands": [
|
"demands": [
|
||||||
{ "col": 3, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 }
|
{ "col": 3, "row": 0, "name": "Depot Royal", "cargo": "wood", "amount": 3, "deadline": 30 }
|
||||||
],
|
],
|
||||||
"walls": [],
|
"walls": [],
|
||||||
"stock": [
|
"stock": [
|
||||||
{ "kind": "rook", "count": 3 }
|
{ "kind": "rook", "count": 3 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
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"];
|
||||||
{
|
|
||||||
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,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 +76,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,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;
|
||||||
|
|
|
||||||
|
|
@ -17,94 +17,199 @@ public partial class LevelSelectScreen : Control
|
||||||
|
|
||||||
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 ---
|
||||||
|
var cardRow = new HBoxContainer();
|
||||||
|
cardRow.Alignment = BoxContainer.AlignmentMode.Center;
|
||||||
|
cardRow.AddThemeConstantOverride("separation", 28);
|
||||||
|
cardRow.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||||
|
cardRow.MouseFilter = MouseFilterEnum.Ignore;
|
||||||
|
|
||||||
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);
|
cardRow.AddChild(CreateLevelCard(i, name, desc));
|
||||||
grid.AddChild(card);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vbox.AddChild(grid);
|
outerVBox.AddChild(cardRow);
|
||||||
margin.AddChild(vbox);
|
|
||||||
panel.AddChild(margin);
|
// Bottom spacer
|
||||||
AddChild(panel);
|
outerVBox.AddChild(new Control
|
||||||
|
{
|
||||||
|
SizeFlagsVertical = SizeFlags.ExpandFill,
|
||||||
|
CustomMinimumSize = new Vector2(0, 40)
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +92,7 @@ public static class LevelLoader
|
||||||
public int Row { get; set; }
|
public int Row { get; set; }
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public string Cargo { get; set; } = "";
|
public string Cargo { get; set; } = "";
|
||||||
public int Interval { get; set; }
|
public int Amount { get; set; } = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DemandDto
|
private class DemandDto
|
||||||
|
|
@ -115,5 +115,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,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,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);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ public enum SimPhase
|
||||||
Edit,
|
Edit,
|
||||||
Running,
|
Running,
|
||||||
Paused,
|
Paused,
|
||||||
Collision,
|
|
||||||
Victory,
|
Victory,
|
||||||
Defeat
|
Defeat
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://ddq5b3ayhu50e
|
|
||||||
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
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://gigwqhqsob8f
|
|
||||||
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)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,9 @@ 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 :
|
3 types. Chaque piece a un **niveau** (I, II, III…) qui determine sa puissance relative au sein d'un meme type. Dans le prototype, toutes les pieces sont de niveau fixe — le systeme de niveaux sera exploite dans les versions futures.
|
||||||
|
|
||||||
|
3 types :
|
||||||
|
|
||||||
#### Tour (niveau II)
|
#### Tour (niveau II)
|
||||||
|
|
||||||
|
|
@ -130,13 +132,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,13 +163,15 @@ 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) :
|
||||||
|
|
@ -174,6 +180,12 @@ Hierarchie de statut social (proto) :
|
||||||
Cavalier 3
|
Cavalier 3
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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 +229,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
|
||||||
|
|
||||||
|
|
@ -673,7 +688,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.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,11 @@ run/main_scene="res://Scenes/Main.tscn"
|
||||||
config/features=PackedStringArray("4.6", "C#", "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