Implement the playable first counting puzzle with committed input, persistence and Korean voice
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using MagnetismHeroes.UnityRuntime.StageAssets;
|
||||
using MagnetismHeroes.UnityRuntime.Bootstrap;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class FirstSliceAssetTests
|
||||
{
|
||||
[Test] public void FirstPuzzleHasAnExecutableAppPrefab()
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
|
||||
Assert.That(prefab, Is.Not.Null, "The first puzzle must have a real Inspector-editable app prefab.");
|
||||
Assert.That(prefab.GetComponent<GameApp>(), Is.Not.Null);
|
||||
var serialized = new SerializedObject(prefab.GetComponent<GameApp>());
|
||||
foreach (var field in new[] { "_homePanel", "_selectionPanel", "_playPanel", "_world", "_instruction", "_counter", "_stageTitle", "_saveNotice",
|
||||
"_playButton", "_continueButton", "_stageOneButton", "_homeButton", "_selectionHomeButton", "_presenter", "_orbit", "_input", "_voice" })
|
||||
Assert.That(serialized.FindProperty(field).objectReferenceValue, Is.Not.Null, field);
|
||||
}
|
||||
[Test] public void StaticKoreanFontContainsEveryVisibleFirstSliceCharacter()
|
||||
{
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/MagnetismHeroes/UnityRuntime/Presentation/Prefabs/GameApp.prefab");
|
||||
var texts = prefab.GetComponentsInChildren<TMP_Text>(true);
|
||||
foreach (var text in texts)
|
||||
{
|
||||
Assert.That(text.font.atlasPopulationMode, Is.EqualTo(AtlasPopulationMode.Static));
|
||||
Assert.That(text.enableAutoSizing, Is.False);
|
||||
foreach (var character in text.text) Assert.That(text.font.HasCharacter(character), Is.True, text.name + ": " + character);
|
||||
}
|
||||
}
|
||||
[Test] public void StageAssetConvertsToAnIndependentDefinition()
|
||||
{
|
||||
var stage = AssetDatabase.LoadAssetAtPath<StageAsset>("Assets/MagnetismHeroes/UnityRuntime/StageAssets/Stage01.asset");
|
||||
var before = EditorJsonUtility.ToJson(stage); var definition = stage.ToDefinition();
|
||||
Assert.That(definition.Id, Is.EqualTo(1)); Assert.That(definition.Total, Is.EqualTo(1));
|
||||
Assert.That(EditorJsonUtility.ToJson(stage), Is.EqualTo(before));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 599347c36cd01ad4fa166c4c6b6f3b77
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class GameSessionTests
|
||||
{
|
||||
private sealed class MemoryStore : IProgressStore
|
||||
{
|
||||
public ProgressSave Save = new ProgressSave();
|
||||
public string LoadWarning;
|
||||
public bool TryLoad(out ProgressSave save, out string error) { save = Save; error = LoadWarning; return true; }
|
||||
public bool TrySave(ProgressSave save, out string error) { Save = save; error = null; return true; }
|
||||
}
|
||||
private static StageDefinition One() => new StageDefinition(1, StageKind.Counting, new[] { 1 }, new[] { 1 });
|
||||
|
||||
[Test] public void LoadRecoveryWarningIsRetainedForDiagnostics()
|
||||
{
|
||||
var session = new GameSession(new[] { One() }, new MemoryStore { LoadWarning = "Recovered valid backup" });
|
||||
Assert.That(session.LastSaveError, Is.EqualTo("Recovered valid backup"));
|
||||
}
|
||||
[Test] public void FirstStageCannotCompleteWithoutACommittedCollection()
|
||||
{
|
||||
var store = new MemoryStore(); var session = new GameSession(new[] { One() }, store);
|
||||
Assert.That(session.StartStage(1), Is.True);
|
||||
var ball = session.Board.Balls.Single(); var group = session.Board.Groups.Single();
|
||||
Assert.That(session.Count(ball.id).Reason, Is.EqualTo(RejectionReason.NotCollected));
|
||||
Assert.That(session.MoveGroup(group.id, new Position3()).Accepted, Is.False);
|
||||
Assert.That(store.Save.completedStageIds, Is.Empty);
|
||||
Assert.That(session.Screen, Is.EqualTo(SessionScreen.Puzzle));
|
||||
}
|
||||
[Test] public void MoveAndCountCreateOneAtomicCompletionAndKeepTheSameBoard()
|
||||
{
|
||||
var store = new MemoryStore(); var session = new GameSession(new[] { One() }, store); session.StartStage(1);
|
||||
var ballId = session.Board.Balls.Single().id; var groupId = session.Board.Groups.Single().id;
|
||||
session.MoveGroup(groupId, new Position3(1, 2, -1));
|
||||
var position = session.Board.Balls.Single().position;
|
||||
Assert.That(session.Count(ballId).Count, Is.EqualTo(1));
|
||||
Assert.That(session.Screen, Is.EqualTo(SessionScreen.FreePlay));
|
||||
Assert.That(store.Save.completedStageIds, Is.EqualTo(new[] { 1 }));
|
||||
Assert.That(store.Save.puzzleStates, Is.Empty);
|
||||
Assert.That(store.Save.latestFreePlay.balls.Single().position, Is.EqualTo(position));
|
||||
Assert.That(session.Count(ballId).Accepted, Is.False);
|
||||
Assert.That(store.Save.completedStageIds, Is.EqualTo(new[] { 1 }));
|
||||
}
|
||||
[Test] public void IncompleteBoardRestoresCommittedPositionCameraAndCollection()
|
||||
{
|
||||
var store = new MemoryStore(); var session = new GameSession(new[] { One() }, store); session.StartStage(1);
|
||||
session.MoveGroup(session.Board.Groups.Single().id, new Position3(1, 2, 3));
|
||||
var camera = GameSession.DefaultCamera(); camera.distance = 11;
|
||||
session.CommitCamera(camera); session.ShowHome();
|
||||
var resumed = new GameSession(new[] { One() }, store); resumed.StartStage(1);
|
||||
Assert.That(resumed.Board.Balls.Single().position, Is.EqualTo(new Position3(1, 2, 3)));
|
||||
Assert.That(resumed.Board.CollectedBallIds.Length, Is.EqualTo(1));
|
||||
Assert.That(resumed.Board.CountedCount, Is.Zero);
|
||||
Assert.That(resumed.Camera.distance, Is.EqualTo(11));
|
||||
Assert.That(resumed.Screen, Is.EqualTo(SessionScreen.Puzzle));
|
||||
}
|
||||
[Test] public void CompletedStageReplayKeepsCompletionAndItsPreviousFreePlaySeparately()
|
||||
{
|
||||
var store = new MemoryStore(); var session = new GameSession(new[] { One() }, store); session.StartStage(1);
|
||||
session.MoveGroup(session.Board.Groups.Single().id, new Position3(2, 0, 0)); session.Count(session.Board.Balls.Single().id);
|
||||
var freePosition = store.Save.latestFreePlay.balls.Single().position;
|
||||
Assert.That(session.IsUnlocked(2), Is.True); Assert.That(session.IsUnlocked(3), Is.False);
|
||||
session.StartStage(1); session.MoveGroup(session.Board.Groups.Single().id, new Position3(1, 0, 0));
|
||||
Assert.That(store.Save.completedStageIds, Is.EqualTo(new[] { 1 }));
|
||||
Assert.That(store.Save.latestFreePlay.balls.Single().position, Is.EqualTo(freePosition));
|
||||
Assert.That(store.Save.puzzleStates.Single().stageId, Is.EqualTo(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39eee6770356bc246bc854f34306e1fb
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MagnetismHeroes.Core;
|
||||
using MagnetismHeroes.UnityRuntime.Persistence;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MagnetismHeroes.Tests
|
||||
{
|
||||
public sealed class LocalProgressStoreTests
|
||||
{
|
||||
private string _directory, _path;
|
||||
private StageDefinition _stage;
|
||||
[SetUp] public void SetUp()
|
||||
{
|
||||
_directory = Path.Combine(Path.GetTempPath(), "MagnetismHeroes-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_directory);
|
||||
_path = Path.Combine(_directory, "progress.json");
|
||||
_stage = new StageDefinition(1, StageKind.Counting, new[] { 1 }, new[] { 1 });
|
||||
}
|
||||
[TearDown] public void TearDown() => Directory.Delete(_directory, true);
|
||||
private LocalProgressStore Store() => new LocalProgressStore(_path, new[] { _stage });
|
||||
private ProgressSave Puzzle()
|
||||
{ var board = new BoardRules(_stage); return new ProgressSave { puzzleStates = new[] { board.ToPuzzleState(GameSession.DefaultCamera()) } }; }
|
||||
|
||||
[Test] public void CommittedPositionAndCameraRoundTripWithoutPartialLoad()
|
||||
{
|
||||
var progress = Puzzle(); progress.puzzleStates[0].balls[0].position = new Position3(1, -2, 3);
|
||||
progress.puzzleStates[0].camera.distance = 10;
|
||||
Assert.That(Store().TrySave(progress, out var error), Is.True, error);
|
||||
Assert.That(Store().TryLoad(out var loaded, out error), Is.True, error);
|
||||
Assert.That(loaded.puzzleStates[0].balls[0].position, Is.EqualTo(new Position3(1, -2, 3)));
|
||||
Assert.That(loaded.puzzleStates[0].camera.distance, Is.EqualTo(10));
|
||||
}
|
||||
[TestCase("position")]
|
||||
[TestCase("x")]
|
||||
[TestCase("camera")]
|
||||
[TestCase("countedBallIds")]
|
||||
[TestCase("latestFreePlay")]
|
||||
public void MissingRequiredFieldsAreRejectedInsteadOfDefaulted(string field)
|
||||
{
|
||||
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
||||
var json = File.ReadAllText(_path).Replace("\"" + field + "\":", "\"removed_" + field + "\":");
|
||||
File.WriteAllText(_path, json);
|
||||
Assert.That(Store().TryLoad(out var loaded, out var error), Is.False);
|
||||
Assert.That(loaded, Is.Null); Assert.That(error, Does.Contain(field));
|
||||
}
|
||||
[Test] public void DuplicateJsonKeysAndTrailingContentAreRejected()
|
||||
{
|
||||
File.WriteAllText(_path, "{\"schemaVersion\":1,\"schemaVersion\":1}");
|
||||
Assert.That(Store().TryLoad(out _, out _), Is.False);
|
||||
File.Delete(_path); Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
||||
File.WriteAllText(_path, File.ReadAllText(_path) + " {}");
|
||||
Assert.That(Store().TryLoad(out _, out _), Is.False);
|
||||
}
|
||||
[Test] public void ValidBackupRecoversWholeBoardAndDamagedOriginalSurvivesNextSave()
|
||||
{
|
||||
var store = Store(); var progress = Puzzle(); Assert.That(store.TrySave(progress, out _), Is.True);
|
||||
progress.puzzleStates[0].camera.distance = 12; Assert.That(store.TrySave(progress, out _), Is.True);
|
||||
File.WriteAllText(_path, "broken-primary");
|
||||
Assert.That(store.TryLoad(out var loaded, out var error), Is.True, error);
|
||||
Assert.That(loaded.puzzleStates[0].camera.distance, Is.EqualTo(8));
|
||||
Assert.That(error, Does.Contain("Recovered"));
|
||||
Assert.That(store.TrySave(loaded, out error), Is.True, error);
|
||||
var damaged = Directory.GetFiles(_directory, "*.damaged-*").Single();
|
||||
Assert.That(File.ReadAllText(damaged), Is.EqualTo("broken-primary"));
|
||||
Assert.That(File.ReadAllText(_path + ".bak"), Does.Not.Contain("broken-primary"));
|
||||
Assert.That(File.Exists(_path + ".tmp"), Is.False);
|
||||
}
|
||||
[Test] public void WithoutValidBackupLoaderDoesNotOverwriteDamagedEvidence()
|
||||
{
|
||||
File.WriteAllText(_path, "broken-primary"); File.WriteAllText(_path + ".bak", "broken-backup");
|
||||
var store = Store(); Assert.That(store.TryLoad(out _, out _), Is.False);
|
||||
Assert.That(File.ReadAllText(_path), Is.EqualTo("broken-primary"));
|
||||
Assert.That(store.TrySave(new ProgressSave(), out var error), Is.True, error);
|
||||
Assert.That(Directory.GetFiles(_directory, "*.damaged-*").Select(File.ReadAllText), Does.Contain("broken-primary"));
|
||||
}
|
||||
[Test] public void InvalidGroupMembershipDoesNotReplaceAValidSave()
|
||||
{
|
||||
var store = Store(); var progress = Puzzle(); Assert.That(store.TrySave(progress, out _), Is.True);
|
||||
var before = File.ReadAllBytes(_path); progress.puzzleStates[0].groups[0].ballIds = new[] { "missing" };
|
||||
Assert.That(store.TrySave(progress, out _), Is.False); Assert.That(File.ReadAllBytes(_path), Is.EqualTo(before));
|
||||
}
|
||||
[TestCase("schemaVersion", "null")]
|
||||
[TestCase("schemaVersion", "\"1\"")]
|
||||
[TestCase("schemaVersion", "1.5")]
|
||||
[TestCase("distance", "null")]
|
||||
[TestCase("distance", "\"8\"")]
|
||||
[TestCase("x", "null")]
|
||||
[TestCase("x", "\"0\"")]
|
||||
public void WrongScalarTypesAreRejected(string field, string value)
|
||||
{
|
||||
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
||||
var json = File.ReadAllText(_path);
|
||||
json = System.Text.RegularExpressions.Regex.Replace(json, "\"" + field + "\":\\s*-?[0-9]+(?:\\.[0-9]+)?", "\"" + field + "\":" + value);
|
||||
File.WriteAllText(_path, json); Assert.That(Store().TryLoad(out var loaded, out _), Is.False); Assert.That(loaded, Is.Null);
|
||||
}
|
||||
[Test] public void SavedJsonContainsOnlyContractFields()
|
||||
{
|
||||
Assert.That(Store().TrySave(Puzzle(), out _), Is.True);
|
||||
Assert.That(File.ReadAllText(_path), Does.Not.Contain("IsValid"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d3b301c187eab84086f1a6504b44942
|
||||
Reference in New Issue
Block a user