using System; using System.Collections.Generic; using System.Linq; namespace MagnetismHeroes.Core { public enum SessionScreen { Home, StageSelection, Puzzle, FreePlay } // Owns committed game state. A gesture preview never reaches this class. public sealed class GameSession { private readonly StageDefinition[] _definitions; private readonly IProgressStore _store; private ProgressSave _progress; public SessionScreen Screen { get; private set; } = SessionScreen.Home; public BoardRules Board { get; private set; } public CameraState Camera { get; private set; } = DefaultCamera(); public string LastSaveError { get; private set; } public bool HasFreePlay => _progress.latestFreePlay != null; public int[] CompletedStageIds => (int[])_progress.completedStageIds.Clone(); public IReadOnlyList Definitions => Array.AsReadOnly(_definitions); public GameSession(IEnumerable definitions, IProgressStore store) { _definitions = definitions.ToArray(); _store = store ?? throw new ArgumentNullException(nameof(store)); if (!_store.TryLoad(out var loaded, out var error)) loaded = new ProgressSave(); LastSaveError = error; if (!SaveValidator.TryValidate(loaded, _definitions, out error)) { loaded = new ProgressSave(); LastSaveError = error; } _progress = loaded; } public bool IsUnlocked(int stageId) => stageId == 1 || _progress.completedStageIds.Contains(stageId) || (stageId >= 2 && stageId <= 30 && _progress.completedStageIds.Contains(stageId - 1)); public void ShowSelection() { SaveCurrent(); Screen = SessionScreen.StageSelection; Board = null; } public void ShowHome() { SaveCurrent(); Screen = SessionScreen.Home; Board = null; } public bool StartStage(int stageId) { var definition = _definitions.FirstOrDefault(value => value.Id == stageId); if (definition == null || !IsUnlocked(stageId)) return false; SaveCurrent(); var restored = _progress.puzzleStates.FirstOrDefault(state => state.stageId == stageId); Board = new BoardRules(definition, restored); Camera = restored == null ? DefaultCamera() : CopyCamera(restored.camera); Screen = SessionScreen.Puzzle; SaveCurrent(); return true; } public bool ContinueFreePlay() { if (_progress.latestFreePlay == null) return false; SaveCurrent(); Board = BoardRules.CreateFreePlay(_progress.latestFreePlay); Camera = CopyCamera(_progress.latestFreePlay.camera); Screen = SessionScreen.FreePlay; return true; } public RuleResult MoveGroup(string groupId, Position3 delta) => Commit(() => Board.TryMoveGroup(groupId, delta)); public RuleResult Merge(string first, string second) => Commit(() => Board.TryMerge(first, second)); public RuleResult Count(string ballId) => Commit(() => Board.TryCount(ballId)); public void CommitCamera(CameraState camera) { // Board snapshot methods validate all camera coordinates and rotation. Camera = Board == null ? CopyCamera(camera) : Board.ToFreePlayState(camera).camera; SaveCurrent(); } private RuleResult Commit(Func mutation) { if (Board == null) return RuleResult.Reject(RejectionReason.InvalidGroup); var result = mutation(); if (!result.Accepted) return result; if (Screen == SessionScreen.Puzzle && Board.IsSolved) { // Completion, removing that unfinished puzzle, and creating free play are one saved update. _progress.completedStageIds = _progress.completedStageIds.Concat(new[] { Board.StageId }).Distinct().OrderBy(id => id).ToArray(); _progress.puzzleStates = _progress.puzzleStates.Where(state => state.stageId != Board.StageId).ToArray(); _progress.latestFreePlay = Board.ToFreePlayState(Camera); Board = BoardRules.CreateFreePlay(_progress.latestFreePlay); Screen = SessionScreen.FreePlay; } SaveCurrent(); return result; } public bool SaveCurrent() { if (Board != null) { if (Screen == SessionScreen.Puzzle) _progress.puzzleStates = _progress.puzzleStates.Where(state => state.stageId != Board.StageId) .Concat(new[] { Board.ToPuzzleState(Camera) }).OrderBy(state => state.stageId).ToArray(); else if (Screen == SessionScreen.FreePlay) _progress.latestFreePlay = Board.ToFreePlayState(Camera); } var success = _store.TrySave(_progress, out var error); LastSaveError = error; return success; } public static CameraState DefaultCamera() => new CameraState { target = new Position3(0, 0.6f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 8 }; private static CameraState CopyCamera(CameraState camera) => new CameraState { target = camera.target, rotation = camera.rotation, distance = camera.distance }; } }