Chessistics/Scripts/Main.cs
Samuel Bouchet e6eaae43ab Initial commit: Chessistics prototype v0.3
Black box sim engine (commands in, events out) with 3 piece types
(Rook, Bishop, Knight), cargo transfer system with social status
priority, collision detection, and victory/defeat conditions.

57 tests covering rules, simulation, loading, and solvability.
Godot 4 presentation layer scaffolding.
2026-04-10 14:58:03 +02:00

438 lines
13 KiB
C#

using Godot;
using System.Collections.Generic;
using Chessistics.Engine.Commands;
using Chessistics.Engine.Events;
using Chessistics.Engine.Loading;
using Chessistics.Engine.Model;
using Chessistics.Engine.Simulation;
using Chessistics.Scripts.Board;
using Chessistics.Scripts.Input;
using Chessistics.Scripts.Pieces;
using Chessistics.Scripts.Presentation;
using Chessistics.Scripts.UI;
namespace Chessistics.Scripts;
public partial class Main : Node2D
{
private GameSim? _sim;
private LevelDef? _currentLevel;
private int _currentLevelIndex;
// Views
private BoardView _boardView = null!;
private InputMapper _inputMapper = null!;
private EventAnimator _eventAnimator = null!;
// UI
private CanvasLayer _uiLayer = null!;
private ObjectivePanel _objectivePanel = null!;
private PieceStockPanel _pieceStockPanel = null!;
private DetailPanel _detailPanel = null!;
private ControlBar _controlBar = null!;
private MetricsOverlay _metricsOverlay = null!;
private LevelSelectScreen _levelSelectScreen = null!;
private Label _levelTitle = null!;
// Simulation timer
private Godot.Timer _simTimer = null!;
private float _simInterval = 1.0f;
private bool _running;
private static readonly string[] LevelFiles = ["level_01.json", "level_02.json", "level_03.json"];
private static readonly Color BackgroundColor = new("#2D2D2D");
public override void _Ready()
{
RenderingServer.SetDefaultClearColor(BackgroundColor);
BuildSceneTree();
ConnectSignals();
ShowLevelSelect();
}
private void BuildSceneTree()
{
// Camera
var camera = new Camera2D { Enabled = true };
AddChild(camera);
// Board
_boardView = new BoardView();
AddChild(_boardView);
// Input
_inputMapper = new InputMapper();
_inputMapper.Initialize(_boardView);
AddChild(_inputMapper);
// Animator
_eventAnimator = new EventAnimator();
AddChild(_eventAnimator);
// Sim timer
_simTimer = new Godot.Timer { OneShot = false, WaitTime = _simInterval };
_simTimer.Timeout += OnSimTimerTick;
AddChild(_simTimer);
// UI Layer
_uiLayer = new CanvasLayer();
AddChild(_uiLayer);
// Level title
_levelTitle = new Label
{
Position = new Vector2(10, 10),
Text = "CHESSISTICS"
};
_levelTitle.AddThemeFontSizeOverride("font_size", 20);
_uiLayer.AddChild(_levelTitle);
// Side panel (right)
var sidePanel = new VBoxContainer
{
Position = new Vector2(700, 50),
CustomMinimumSize = new Vector2(200, 500)
};
_objectivePanel = new ObjectivePanel();
sidePanel.AddChild(_objectivePanel);
sidePanel.AddChild(new HSeparator());
_pieceStockPanel = new PieceStockPanel();
sidePanel.AddChild(_pieceStockPanel);
_detailPanel = new DetailPanel();
sidePanel.AddChild(_detailPanel);
_uiLayer.AddChild(sidePanel);
// Control bar (bottom)
_controlBar = new ControlBar
{
Position = new Vector2(10, 600)
};
_uiLayer.AddChild(_controlBar);
// Metrics overlay (center)
_metricsOverlay = new MetricsOverlay
{
Position = new Vector2(200, 150),
CustomMinimumSize = new Vector2(300, 250)
};
_uiLayer.AddChild(_metricsOverlay);
// Level select screen
_levelSelectScreen = new LevelSelectScreen();
_levelSelectScreen.SetAnchorsPreset(Control.LayoutPreset.FullRect);
_uiLayer.AddChild(_levelSelectScreen);
// Initialize animator
_eventAnimator.Initialize(_boardView, _objectivePanel, _controlBar, _metricsOverlay);
}
private void ConnectSignals()
{
_levelSelectScreen.LevelSelected += OnLevelSelected;
_pieceStockPanel.PieceSelected += OnPieceKindSelected;
_inputMapper.PlacementRequested += OnPlacementRequested;
_inputMapper.Cancelled += OnPlacementCancelled;
_controlBar.PlayPressed += OnPlay;
_controlBar.PausePressed += OnPause;
_controlBar.StepPressed += OnStep;
_controlBar.StopPressed += OnStop;
_controlBar.SpeedChanged += OnSpeedChanged;
_eventAnimator.TurnAnimationCompleted += OnTurnAnimationCompleted;
_eventAnimator.VictoryReached += OnVictory;
_eventAnimator.CollisionOccurred += OnCollision;
_metricsOverlay.RetryPressed += OnRetry;
_metricsOverlay.NextLevelPressed += OnNextLevel;
_detailPanel.RemoveRequested += OnRemoveRequested;
_inputMapper.CellClicked += OnCellClicked;
}
private void OnCellClicked(int col, int row)
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
if (snap.Phase != SimPhase.Edit) return;
var coords = new Coords(col, row);
var piece = snap.Pieces.FirstOrDefault(p => p.StartCell == coords || p.EndCell == coords);
if (piece != null)
_detailPanel.ShowPiece(piece);
else
_detailPanel.Hide();
}
// --- Level Management ---
private void ShowLevelSelect()
{
_levelSelectScreen.Visible = true;
_boardView.Visible = false;
}
private void OnLevelSelected(int levelIndex)
{
_currentLevelIndex = levelIndex;
LoadLevel(levelIndex);
}
private void LoadLevel(int index)
{
if (index < 0 || index >= LevelFiles.Length) return;
var path = $"res://Data/levels/{LevelFiles[index]}";
var file = Godot.FileAccess.Open(path, Godot.FileAccess.ModeFlags.Read);
if (file == null)
{
GD.PrintErr($"Cannot open level file: {path}");
return;
}
var json = file.GetAsText();
file.Close();
_currentLevel = LevelLoader.Load(json);
_sim = new GameSim(_currentLevel);
_levelSelectScreen.Visible = false;
_boardView.Visible = true;
_boardView.BuildBoard(_currentLevel);
_objectivePanel.Setup(_currentLevel.Demands);
_pieceStockPanel.Setup(_currentLevel.Stock);
_controlBar.UpdateForPhase(SimPhase.Edit);
_controlBar.ResetTurn();
_metricsOverlay.Hide();
_detailPanel.Hide();
_eventAnimator.ClearAll();
_levelTitle.Text = $"CHESSISTICS — {_currentLevel.Name}";
// Center camera on board
var cam = GetNode<Camera2D>("Camera2D");
cam.Position = new Vector2(
_currentLevel.Width * BoardView.CellSize / 2f,
-_currentLevel.Height * BoardView.CellSize / 2f
);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
}
// --- Edit Phase ---
private void OnPieceKindSelected(int kindIndex)
{
_inputMapper.SelectPieceKind((PieceKind)kindIndex);
}
private void OnPlacementRequested(int kindIndex, int startCol, int startRow, int endCol, int endRow)
{
if (_sim == null) return;
var kind = (PieceKind)kindIndex;
var start = new Coords(startCol, startRow);
var end = new Coords(endCol, endRow);
var events = _sim.ProcessCommand(new PlacePieceCommand(kind, start, end));
HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
}
private void OnPlacementCancelled()
{
_pieceStockPanel.ClearSelection();
}
private void OnRemoveRequested(int pieceId)
{
if (_sim == null) return;
var events = _sim.ProcessCommand(new RemovePieceCommand(pieceId));
HandleEditEvents(events);
_inputMapper.SetSnapshot(_sim.GetSnapshot());
}
private void HandleEditEvents(IReadOnlyList<IWorldEvent> events)
{
foreach (var evt in events)
{
switch (evt)
{
case PiecePlacedEvent placed:
CreatePieceVisual(placed);
UpdateStockFromSnapshot();
break;
case PieceRemovedEvent removed:
_eventAnimator.UnregisterPiece(removed.PieceId);
UpdateStockFromSnapshot();
_detailPanel.Hide();
break;
case PlacementRejectedEvent rejected:
GD.Print($"Placement rejected: {rejected.Reason}");
break;
case CommandRejectedEvent rejected:
GD.Print($"Command rejected: {rejected.Reason}");
break;
}
}
}
private void CreatePieceVisual(PiecePlacedEvent placed)
{
var pieceView = new PieceView();
pieceView.Setup(placed.PieceId, placed.Kind, placed.Start, placed.End, _boardView);
_boardView.AddChild(pieceView);
var color = placed.Kind switch
{
PieceKind.Rook => new Color("#4A7AB5"),
PieceKind.Bishop => new Color("#B54A8E"),
PieceKind.Knight => new Color("#B5824A"),
_ => Colors.White
};
var trajectView = new TrajectView();
trajectView.Setup(placed.PieceId,
_boardView.CoordsToPixel(placed.Start),
_boardView.CoordsToPixel(placed.End),
color);
_boardView.AddChild(trajectView);
_eventAnimator.RegisterPiece(placed.PieceId, pieceView, trajectView);
}
private void UpdateStockFromSnapshot()
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
foreach (var (kind, remaining) in snap.RemainingStock)
_pieceStockPanel.UpdateCount(kind, remaining);
}
// --- Exec Phase ---
private void OnPlay()
{
if (_sim == null) return;
var snap = _sim.GetSnapshot();
if (snap.Phase == SimPhase.Edit)
{
var events = _sim.ProcessCommand(new StartSimulationCommand());
foreach (var evt in events)
{
if (evt is CommandRejectedEvent r)
{
GD.Print($"Cannot start: {r.Reason}");
return;
}
}
}
else if (snap.Phase == SimPhase.Paused)
{
_sim.ProcessCommand(new ResumeSimulationCommand());
}
_running = true;
_controlBar.UpdateForPhase(SimPhase.Running);
_simTimer.WaitTime = _simInterval;
_simTimer.Start();
}
private void OnPause()
{
if (_sim == null) return;
_sim.ProcessCommand(new PauseSimulationCommand());
_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 || phase == SimPhase.Collision)
{
_running = false;
_simTimer.Stop();
}
}
private void OnVictory()
{
_running = false;
_simTimer.Stop();
}
private void OnCollision()
{
_running = false;
_simTimer.Stop();
_controlBar.UpdateForPhase(SimPhase.Collision);
}
// --- Navigation ---
private void OnRetry()
{
LoadLevel(_currentLevelIndex);
}
private void OnNextLevel()
{
if (_currentLevelIndex + 1 < LevelFiles.Length)
LoadLevel(_currentLevelIndex + 1);
else
ShowLevelSelect();
}
}