Prove actual cancellable drags for every original stage sphere

This commit is contained in:
NINI
2026-10-10 06:34:33 +09:00
parent 53d5e8a2d5
commit 971a792bfd
@@ -330,11 +330,20 @@ namespace MagnetismHeroes.Tests
foreach (var collider in _disabledScenery) if (collider != null) collider.enabled = true;
Physics.SyncTransforms();
var discoveries = new System.Collections.Generic.List<string>();
var dragProof = new System.Collections.Generic.List<string>();
int totalDragged = 0;
for (int stage = 1; stage <= 30; stage++)
{
_app.StartStage(stage); _app.Voice.Clear(); yield return null;
var before = _app.Session.Board.Balls.Select(ball => (ball.id, ball.position)).ToArray();
var visible = new System.Collections.Generic.HashSet<string>(); var original = _app.Orbit.Snapshot();
var dragged = new System.Collections.Generic.HashSet<string>();
var positions = _app.Session.Board.Balls.ToDictionary(ball => ball.id, ball => BoardPresenter.ToVector(ball.position));
var boardSnapshot = JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera));
var saved = JsonUtility.ToJson(_store.Save); int writes = _store.Writes;
WriteRecords($"initial-stage-{stage}-board-before.json", new[] { boardSnapshot });
WriteRecords($"initial-stage-{stage}-save-before.json", new[] { saved });
var grabField = _app.Input.GetType().GetField("_ballId", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
foreach (float pitch in new[] { 15f, 35f, 60f, 80f })
for (float yaw = 0; yaw < 360; yaw += 15)
{
@@ -366,12 +375,53 @@ namespace MagnetismHeroes.Tests
readable &= !Physics.Raycast(glyphRay, Vector3.Distance(camera.transform.position, glyphCenter) - .01f);
if (!readable) continue;
if (visible.Add(ball.id)) discoveries.Add($"stage={stage};id={ball.id};position={world};camera={camera.transform.position};pitch={pitch};yaw={yaw};visibleGlyph={number.text};glyph={number.transform.position}");
if (dragged.Contains(ball.id) || view.x < .221f || view.x > .771f || view.y < .306f || view.y > .741f) continue;
foreach (var direction in new[] { Vector2.up, Vector2.down, Vector2.left, Vector2.right })
{
var manual = _app.Orbit.Snapshot(); var manualJson = JsonUtility.ToJson(manual);
var point = (Vector2)camera.WorldToScreenPoint(world); var end = point + direction * 40;
var endView = new Vector2(end.x / Screen.width, end.y / Screen.height);
if (endView.x < .221f || endView.x > .771f || endView.y < .306f || endView.y > .741f) continue;
_app.Input.Begin(point, false); Assert.That(grabField.GetValue(_app.Input), Is.EqualTo(ball.id), "Begin must grab this exact initial sphere ID.");
_app.Input.Move(end);
var preview = _app.Presenter.Balls[ball.id].transform.position;
float pixels = Vector2.Distance(camera.WorldToScreenPoint(preview), point); var delta = _app.Presenter.DropDelta;
bool moved = pixels >= 25 && Vector3.Distance(preview, world) >= .05f && _app.Orbit.IsAssisting;
Assert.That(_app.Presenter.CanDropGroup(group.id), Is.True, "Every whole-sphere preview remains safe against the actual ground and scenery.");
Assert.That(_app.Presenter.Balls.Keys, Is.EquivalentTo(positions.Keys));
foreach (var member in _app.Session.Board.Balls)
Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id] + (group.ballIds.Contains(member.id) ? delta : Vector3.zero)), "Only the grabbed whole group translates, preserving its shape.");
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot));
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson));
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
var attempt = $"stage={stage};id={ball.id};grabbed={grabField.GetValue(_app.Input)};group={group.id};members={string.Join(",", group.ballIds)};initial={world};preview={preview};delta={delta};pointerStart={point};pointerEnd={end};pointerStartViewport={view};pointerEndViewport={endView};previewWorldDistance={Vector3.Distance(preview, world):R};previewPixels={pixels:R};passed={moved};manual={manualJson};coreUnchanged=True;saveWrites={writes};sceneryEnabled={_app.Environment.GetComponentsInChildren<Collider>().Count(item => item.enabled && item.gameObject.activeInHierarchy && !item.isTrigger)}";
_app.Input.Cancel();
foreach (var member in _app.Session.Board.Balls) Assert.That(_app.Presenter.Balls[member.id].transform.position, Is.EqualTo(positions[member.id]), "Cancel restores every initial sphere exactly.");
Assert.That(_app.Presenter.DropDelta, Is.EqualTo(Vector3.zero)); Assert.That(_app.Presenter.MergeCandidate, Is.Null);
Assert.That(JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)), Is.EqualTo(boardSnapshot));
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
Assert.That(JsonUtility.ToJson(_app.Orbit.Snapshot()), Is.EqualTo(manualJson));
// The camera has not advanced a frame between Move and Cancel; verify the live return settles without a fixed sleep.
float deadline = Time.realtimeSinceStartup + 2;
while (_app.Orbit.IsReturning && Time.realtimeSinceStartup < deadline) yield return null;
Assert.That(_app.Orbit.IsReturning, Is.False);
Assert.That(Vector3.Distance(camera.transform.position, BoardPresenter.ToVector(manual.target) + Vector3.up * .65f
- new Quaternion(manual.rotation.x, manual.rotation.y, manual.rotation.z, manual.rotation.w) * Vector3.forward * manual.distance), Is.LessThan(.0011f));
Assert.That(JsonUtility.ToJson(_store.Save), Is.EqualTo(saved)); Assert.That(_store.Writes, Is.EqualTo(writes));
dragProof.Add(attempt + ";cancelRestored=True;manualRestored=True;idsShapeMembershipPreserved=True;saveUnchanged=True");
WriteRecords("initial-173-drag-attempts.txt", dragProof);
if (moved) { dragged.Add(ball.id); totalDragged++; break; }
}
}
}
Assert.That(visible, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Actual sphere first-hit discovery stage " + stage);
WriteRecords($"initial-stage-{stage}-board-after.json", new[] { JsonUtility.ToJson(_app.Session.Board.ToPuzzleState(_app.Session.Camera)) });
Assert.That(dragged, Is.EquivalentTo(_app.Session.Board.Balls.Select(ball => ball.id)), "Every original sphere ID must actually move at least 25 screen pixels through Input, then cancel without committing stage " + stage);
Assert.That(_app.Session.Board.Balls.Select(ball => (ball.id, ball.position)), Is.EqualTo(before));
}
Assert.That(totalDragged, Is.EqualTo(173));
WriteRecords("thirty-stage-first-hit-discovery.txt", discoveries);
WriteRecords("initial-173-drag-attempts.txt", dragProof);
}
[UnityTest] public IEnumerator ObstructedReturnRetracesTheActualSafeAssistPathWithoutCameraSnap()
{