Files
MagnetismHeroes/Assets/MagnetismHeroes/Core/GameSession.cs
T

217 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace MagnetismHeroes.Core
{
public enum SessionScreen { Home, StageSelection, Puzzle, FreePlay }
public sealed class BoardRestore
{
public BoardRules Board { get; }
public CameraState Camera { get; }
public bool ResetToInitial { get; }
public BoardRestore(BoardRules board, CameraState camera, bool resetToInitial = false)
{ Board = board; Camera = camera; ResetToInitial = resetToInitial; }
}
// 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 event Action<string> SaveFinished;
public bool HasFreePlay => _progress.latestFreePlay != null;
public int[] CompletedStageIds => (int[])_progress.completedStageIds.Clone();
public IReadOnlyList<StageDefinition> Definitions => Array.AsReadOnly(_definitions);
public bool HasUnfinishedStage(int stageId) => _progress.puzzleStates.Any(state => state.stageId == stageId);
public bool HasSeenTutorial(string id) => _progress.seenTutorialIds.Contains(id);
public BoardHint Hint => BoardHint.Find(Board, _definitions.FirstOrDefault(stage => stage.Id == Board?.StageId));
public bool HasNextStage => Screen == SessionScreen.FreePlay && _definitions.Any(stage => stage.Id == Board.StageId + 1) && IsUnlocked(Board.StageId + 1);
public bool StartNextStage() => HasNextStage && StartStage(Board.StageId + 1);
public bool MarkTutorialSeen(string id)
{
if ((id != "merge" && id != "split") || HasSeenTutorial(id)) return false;
_progress.seenTutorialIds = _progress.seenTutorialIds.Concat(new[] { id }).ToArray();
SaveCurrent(); return true;
}
public GameSession(IEnumerable<StageDefinition> 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, Func<BoardRules, CameraState, BoardRestore> prepare = null)
{
var definition = _definitions.FirstOrDefault(value => value.Id == stageId);
if (definition == null || !IsUnlocked(stageId)) return false;
var next = CopyProgress(); SnapshotCurrent(next);
var restored = next.puzzleStates.FirstOrDefault(state => state.stageId == stageId);
return Enter(next, new BoardRules(definition, restored), restored == null ? DefaultCamera() : CopyCamera(restored.camera),
SessionScreen.Puzzle, prepare);
}
public bool ContinueFreePlay(Func<BoardRules, CameraState, BoardRestore> prepare = null)
{
if (_progress.latestFreePlay == null) return false;
var next = CopyProgress(); SnapshotCurrent(next);
return Enter(next, BoardRules.CreateFreePlay(next.latestFreePlay), CopyCamera(next.latestFreePlay.camera), SessionScreen.FreePlay, prepare);
}
private bool Enter(ProgressSave next, BoardRules restored, CameraState camera, SessionScreen screen,
Func<BoardRules, CameraState, BoardRestore> prepare)
{
var prepared = prepare == null ? new BoardRestore(restored, camera) : prepare(restored, camera);
if (!ValidPreparation(restored, prepared)) return false;
if (screen == SessionScreen.Puzzle)
{
var existing = next.puzzleStates.FirstOrDefault(state => state.stageId == restored.StageId);
if (existing != null && !prepared.ResetToInitial)
{ ApplyPreparedPositions(existing.balls, prepared.Board); existing.camera = CopyCamera(prepared.Camera); }
else if (existing != null)
next.puzzleStates = next.puzzleStates.Select(state => state.stageId == restored.StageId ? prepared.Board.ToPuzzleState(prepared.Camera) : state).ToArray();
else next.puzzleStates = next.puzzleStates.Concat(new[] { prepared.Board.ToPuzzleState(prepared.Camera) }).OrderBy(state => state.stageId).ToArray();
}
else if (!prepared.ResetToInitial)
{ ApplyPreparedPositions(next.latestFreePlay.balls, prepared.Board); next.latestFreePlay.camera = CopyCamera(prepared.Camera); }
else next.latestFreePlay = prepared.Board.ToFreePlayState(prepared.Camera);
if (!SaveValidator.TryValidateUpdate(_progress, next, _definitions, out _)) return false;
var saved = _store.TrySave(next, out var error);
LastSaveError = error; SaveFinished?.Invoke(error);
if (!saved) return false;
_progress = next; Board = prepared.Board; Camera = CopyCamera(prepared.Camera); Screen = screen;
return true;
}
private static void ApplyPreparedPositions(BallState[] balls, BoardRules prepared)
{
var positions = prepared.Balls.ToDictionary(ball => ball.id, ball => ball.position);
foreach (var ball in balls) ball.position = positions[ball.id];
}
private bool ValidPreparation(BoardRules restored, BoardRestore prepared)
{
if (prepared?.Board == null || !SaveValidator.ValidCamera(prepared.Camera)
|| prepared.Board.StageId != restored.StageId || prepared.Board.Mode != restored.Mode) return false;
var expected = restored;
if (prepared.ResetToInitial)
{
expected = new BoardRules(_definitions.First(stage => stage.Id == restored.StageId));
if (restored.Mode == BoardMode.FreePlay) expected = BoardRules.CreateFreePlay(expected.ToFreePlayState(DefaultCamera()));
}
if (!expected.CollectedBallIds.SequenceEqual(prepared.Board.CollectedBallIds)
|| !expected.CountedBallIds.SequenceEqual(prepared.Board.CountedBallIds)) return false;
var sourceGroups = expected.Groups; var groups = prepared.Board.Groups;
if (sourceGroups.Length != groups.Length) return false;
for (int index = 0; index < groups.Length; index++)
if (sourceGroups[index].id != groups[index].id || !sourceGroups[index].ballIds.SequenceEqual(groups[index].ballIds)) return false;
var source = expected.Balls.ToDictionary(ball => ball.id, ball => ball.position);
var target = prepared.Board.Balls.ToDictionary(ball => ball.id, ball => ball.position);
if (source.Count != target.Count || source.Keys.Except(target.Keys).Any()) return false;
foreach (var group in groups)
{
var first = group.ballIds[0]; var before = source[first]; var after = target[first];
var delta = new Position3(after.x - before.x, after.y - before.y, after.z - before.z);
if (prepared.ResetToInitial && (delta.x != 0 || delta.y != 0 || delta.z != 0)) return false;
foreach (var id in group.ballIds)
if (Math.Abs(target[id].x - source[id].x - delta.x) > .0001f
|| Math.Abs(target[id].y - source[id].y - delta.y) > .0001f
|| Math.Abs(target[id].z - source[id].z - delta.z) > .0001f) return false;
}
return true;
}
private ProgressSave CopyProgress() => new ProgressSave {
schemaVersion = _progress.schemaVersion, completedStageIds = (int[])_progress.completedStageIds.Clone(),
seenTutorialIds = (string[])_progress.seenTutorialIds.Clone(),
puzzleStates = _progress.puzzleStates.Select(state => new PuzzleState { stageId = state.stageId,
balls = CopyBalls(state.balls), groups = CopyGroups(state.groups), camera = CopyCamera(state.camera),
collectedBallIds = (string[])state.collectedBallIds.Clone(), countedBallIds = (string[])state.countedBallIds.Clone() }).ToArray(),
latestFreePlay = _progress.latestFreePlay == null ? null : new FreePlayState { sourceStageId = _progress.latestFreePlay.sourceStageId,
balls = CopyBalls(_progress.latestFreePlay.balls), groups = CopyGroups(_progress.latestFreePlay.groups), camera = CopyCamera(_progress.latestFreePlay.camera) } };
private static BallState[] CopyBalls(BallState[] balls) => balls.Select(ball => new BallState { id = ball.id, position = ball.position }).ToArray();
private static GroupState[] CopyGroups(GroupState[] groups) => groups.Select(group => new GroupState { id = group.id, ballIds = (string[])group.ballIds.Clone() }).ToArray();
private void SnapshotCurrent(ProgressSave progress)
{
if (Board == null) return;
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);
}
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 MoveAndMerge(string groupId, Position3 delta, string targetGroupId) => Commit(() => Board.TryMoveAndMerge(groupId, delta, targetGroupId));
public RuleResult SplitAndMove(string groupId, IReadOnlyList<string> selectedIds, string newGroupId, Position3 delta) =>
Commit(() => Board.TrySplitAndMove(groupId, selectedIds, newGroupId, delta));
public RuleResult Count(string ballId) => Commit(() => Board.TryCount(ballId));
public RuleResult ReturnGroup(string groupId) => Commit(() => Board.TryReturn(groupId));
public RuleResult Spawn(Position3 position)
{
if (Board == null) return RuleResult.Reject(RejectionReason.InvalidGroup);
int index = 1;
while (Board.Balls.Any(ball => ball.id == $"free-b{index:D2}") || Board.Groups.Any(group => group.id == $"free-g{index:D2}")) index++;
return Commit(() => Board.TrySpawn($"free-b{index:D2}", $"free-g{index:D2}", position));
}
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<RuleResult> 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()
{
SnapshotCurrent(_progress);
var success = _store.TrySave(_progress, out var error);
LastSaveError = error;
SaveFinished?.Invoke(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 };
}
}