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.
438 lines
13 KiB
C#
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();
|
|
}
|
|
}
|