Завершить инженерный кандидат Stage 5

This commit is contained in:
2026-08-19 02:04:45 +03:00
parent b840684ef3
commit cde886ca96
45 changed files with 3236 additions and 98 deletions
@@ -50,4 +50,17 @@ public final class AccessibilitySettingsTest {
assertEquals(24f, AccessibilitySettings.defaults().canvasTextSize(24f), 0f);
}
@Test
public void reducedMotionIsExplicitReversibleAndRestorable() {
AccessibilitySettings settings = AccessibilitySettings.defaults();
assertFalse(settings.reducedMotionEnabled());
assertEquals("Off", settings.reducedMotionLabel());
settings = settings.toggleReducedMotion();
assertTrue(settings.reducedMotionEnabled());
assertEquals("On", settings.reducedMotionLabel());
assertFalse(settings.toggleReducedMotion().reducedMotionEnabled());
assertTrue(AccessibilitySettings.restore(0, false, true).reducedMotionEnabled());
}
}
@@ -0,0 +1,31 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import org.junit.Test;
public final class AudioCueSequencerTest {
@Test
public void multiVariantDeathVoicesNeverRepeatImmediately() {
AudioCueSequencer sequencer = new AudioCueSequencer(17L);
int previous = sequencer.nextVariant(AudioCue.DEATH_VOICE);
for (int i = 0; i < 30; i++) {
int next = sequencer.nextVariant(AudioCue.DEATH_VOICE);
assertNotEquals(previous, next);
previous = next;
}
}
@Test
public void singleVariantCuesRemainStableAndChannelsAreExplicit() {
AudioCueSequencer sequencer = new AudioCueSequencer(9L);
assertEquals(0, sequencer.nextVariant(AudioCue.UI_TAP));
assertEquals(0, sequencer.nextVariant(AudioCue.UI_TAP));
assertEquals(AudioCue.Channel.EFFECTS, AudioCue.JUMP.channel());
assertEquals(AudioCue.Channel.VOICE, AudioCue.DEATH_VOICE.channel());
assertEquals(AudioCue.Channel.MUSIC, AudioCue.MUSIC_PULSE.channel());
}
}
@@ -0,0 +1,35 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public final class AudioSettingsTest {
@Test
public void musicEffectsAndVoiceVolumesCycleIndependently() {
AudioSettings settings = AudioSettings.defaults();
assertEquals(0.66f, settings.musicVolume(), 0f);
assertEquals(1f, settings.effectsVolume(), 0f);
assertEquals(1f, settings.voiceVolume(), 0f);
AudioSettings quieterMusic = settings.nextMusicVolume();
assertEquals(1f, quieterMusic.musicVolume(), 0f);
assertEquals(settings.effectsVolume(), quieterMusic.effectsVolume(), 0f);
assertEquals(settings.voiceVolume(), quieterMusic.voiceVolume(), 0f);
AudioSettings mutedEffects = settings.nextEffectsVolume();
assertEquals(0f, mutedEffects.effectsVolume(), 0f);
assertEquals(settings.musicVolume(), mutedEffects.musicVolume(), 0f);
}
@Test
public void invalidPersistedPresetsRestoreToSafeDefaults() {
AudioSettings restored = AudioSettings.restore(-4, 99, 2);
assertEquals(AudioSettings.defaults().musicPreset(), restored.musicPreset());
assertEquals(AudioSettings.defaults().effectsPreset(), restored.effectsPreset());
assertEquals(2, restored.voicePreset());
assertEquals("66%", restored.volumeLabel(restored.voicePreset()));
}
}
@@ -0,0 +1,57 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class CameraControllerTest {
@Test
public void followUsesHorizontalAndVerticalDampingWithinWorldBounds() {
CameraController camera = new CameraController(1_920f, 1_080f);
camera.reset();
CameraController.Frame frame = camera.update(
0.25f, 1_600f, 1_250f, 1, 3_200f, 1_800f, false);
assertTrue(frame.x() > 0f && frame.x() <= 1_280f);
assertTrue(frame.y() > 0f && frame.y() <= 720f);
CameraController.Frame clamped = camera.update(
5f, 9_000f, 9_000f, 1, 3_200f, 1_800f, false);
assertTrue(clamped.x() <= 1_280f);
assertTrue(clamped.y() <= 720f);
}
@Test
public void temporaryFocusIsBoundedAndExpiresBackToPlayerFollow() {
CameraController camera = new CameraController(1_920f, 1_080f);
camera.reset();
camera.requestFocus(2_500f, 400f, 0.35f);
CameraController.Frame focused = camera.update(
0.1f, 700f, 780f, 1, 3_200f, 1_080f, false);
assertTrue(focused.focusActive());
assertTrue(focused.focusOffsetX() <= CameraController.MAX_FOCUS_OFFSET);
CameraController.Frame expired = camera.update(
0.4f, 700f, 780f, 1, 3_200f, 1_080f, false);
assertFalse(expired.focusActive());
}
@Test
public void impactShakeIsBoundedAndReducedMotionDisablesIt() {
CameraController camera = new CameraController(1_920f, 1_080f);
camera.reset();
camera.impact(24f);
CameraController.Frame shaking = camera.update(
0.016f, 700f, 780f, 1, 3_200f, 1_080f, false);
assertTrue(Math.abs(shaking.shakeX()) <= CameraController.MAX_SHAKE);
assertTrue(Math.abs(shaking.shakeY()) <= CameraController.MAX_SHAKE);
CameraController.Frame reduced = camera.update(
0.016f, 700f, 780f, 1, 3_200f, 1_080f, true);
assertTrue(reduced.shakeX() == 0f);
assertTrue(reduced.shakeY() == 0f);
}
}
@@ -0,0 +1,69 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import org.junit.Test;
public final class DeterministicInputTapeTest {
@Test
public void oneRecordedCommandTapeReplaysToTheSameTerminalSignature() {
DeterministicInputTape tape = DeterministicInputTape.builder(2)
.record(false, true, false, 16L)
.record(false, true, true, 32L)
.record(false, false, false, 16L)
.build();
FakeTarget first = new FakeTarget();
FakeTarget second = new FakeTarget();
assertEquals(tape.play(first), tape.play(second));
assertEquals("level=2;ticks=64;mask=0", first.terminalSignature());
assertEquals(3, tape.commands().size());
}
@Test
public void tapeIdIsStableButChangesWithCommands() {
DeterministicInputTape first = DeterministicInputTape.builder(1)
.record(false, true, false, 16L).build();
DeterministicInputTape same = DeterministicInputTape.builder(1)
.record(false, true, false, 16L).build();
DeterministicInputTape changed = DeterministicInputTape.builder(1)
.record(false, true, true, 16L).build();
assertEquals(first.id(), same.id());
assertNotEquals(first.id(), changed.id());
}
@Test
public void terminalStateSignatureIncludesPuzzleBytesAndCompletionFlags() {
byte[] puzzle = {4, 8, 15, 16, 23, 42};
assertEquals(ReplayStateSignature.of(puzzle, true, false),
ReplayStateSignature.of(puzzle, true, false));
assertNotEquals(ReplayStateSignature.of(puzzle, true, false),
ReplayStateSignature.of(puzzle, false, false));
}
private static final class FakeTarget implements DeterministicInputTape.Target {
private int level;
private long ticks;
private int mask;
@Override
public void startLevel(int oneBasedLevelNumber) { level = oneBasedLevelNumber; }
@Override
public void setInput(boolean left, boolean right, boolean jump) {
mask = (left ? 1 : 0) | (right ? 2 : 0) | (jump ? 4 : 0);
}
@Override
public void advance(long milliseconds) { ticks += milliseconds; }
@Override
public String terminalSignature() {
return "level=" + level + ";ticks=" + ticks + ";mask=" + mask;
}
}
}
@@ -0,0 +1,33 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class PerformanceMonitorTest {
@Test
public void rollingP95AndComplexityAreComparedWithPublishedBudgets() {
PerformanceMonitor monitor = new PerformanceMonitor(240);
for (int frame = 0; frame < 240; frame++) {
monitor.recordFrame(16f, 3f, 7f);
monitor.recordComplexity(6, 180, 900, 48L * 1024L * 1024L);
}
PerformanceMonitor.Snapshot healthy = monitor.snapshot();
assertEquals(240, healthy.sampleCount());
assertEquals(16f, healthy.p95FrameIntervalMs(), 0f);
assertTrue(healthy.within(PerformanceBudget.release()));
assertEquals(6, healthy.maximumBodies());
for (int frame = 0; frame < 24; frame++) {
monitor.recordFrame(75f, 18f, 30f);
monitor.recordComplexity(17, 4_000, 3_000, 140L * 1024L * 1024L);
}
PerformanceMonitor.Snapshot overloaded = monitor.snapshot();
assertFalse(overloaded.within(PerformanceBudget.release()));
assertTrue(overloaded.failureSummary(PerformanceBudget.release()).contains("frameP95"));
assertTrue(overloaded.failureSummary(PerformanceBudget.release()).contains("bodies"));
}
}
@@ -0,0 +1,23 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class PhysicsConfigTest {
@Test
public void releasePhysicsIsOneVersionedFiniteSourceOfTruth() {
PhysicsConfig physics = PhysicsConfig.release();
assertEquals(1, physics.version());
assertEquals(1f / 60f, physics.fixedStepSeconds(), 0f);
assertEquals(1_800f, physics.gravity(), 0f);
assertEquals(370f, physics.runSpeed(), 0f);
assertEquals(880f, physics.jumpSpeed(), 0f);
assertEquals(0.13f, physics.coyoteSeconds(), 0f);
assertEquals(0.14f, physics.jumpBufferSeconds(), 0f);
assertEquals(0.78f, physics.respawnSeconds(), 0f);
assertTrue(physics.maximumBodies() >= 10);
}
}
@@ -0,0 +1,81 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Arrays;
import org.junit.Test;
public final class VersionedSessionCodecTest {
@Test
public void currentSnapshotRoundTripsEveryPuzzleCriticalField() {
GameSessionSnapshot original = sampleSnapshot();
VersionedSessionCodec.DecodeResult decoded =
VersionedSessionCodec.decode(VersionedSessionCodec.encode(original));
assertEquals(VersionedSessionCodec.Status.RESTORED, decoded.status());
GameSessionSnapshot restored = decoded.snapshot();
assertEquals(5, restored.levelNumber());
assertEquals(12.75f, restored.elapsedSeconds(), 0f);
assertEquals(2, restored.deaths());
assertEquals(640f, restored.player().x(), 0f);
assertFalse(restored.player().alive());
assertEquals(2, restored.bodies().size());
assertTrue(restored.bodies().get(0).pinned());
assertTrue(restored.bodies().get(1).conductive());
assertEquals(Arrays.asList(1.25f, 3.5f), restored.platformMotionTimes());
assertTrue(restored.toggleSignals().get("gate"));
assertEquals(2.25f, restored.timedSignals().get("clock"), 0f);
assertTrue(restored.previousSwitchContacts().get("gate@420.0"));
}
@Test
public void legacyV1MigratesWithSafeEmptyEdgeContacts() {
VersionedSessionCodec.DecodeResult decoded = VersionedSessionCodec.decode(
VersionedSessionCodec.encodeLegacyV1(sampleSnapshot()));
assertEquals(VersionedSessionCodec.Status.MIGRATED, decoded.status());
assertEquals(VersionedSessionCodec.CURRENT_VERSION,
decoded.snapshot().saveVersion());
assertTrue(decoded.snapshot().previousSwitchContacts().isEmpty());
assertEquals(2, decoded.snapshot().bodies().size());
}
@Test
public void corruptAndFutureSnapshotsFailClosed() {
VersionedSessionCodec.DecodeResult corrupt =
VersionedSessionCodec.decode(new byte[] {1, 2, 3});
VersionedSessionCodec.DecodeResult future =
VersionedSessionCodec.decode(VersionedSessionCodec.encodeVersionForTest(99));
assertEquals(VersionedSessionCodec.Status.CORRUPT, corrupt.status());
assertNull(corrupt.snapshot());
assertEquals(VersionedSessionCodec.Status.UNSUPPORTED, future.status());
assertNull(future.snapshot());
}
private static GameSessionSnapshot sampleSnapshot() {
return GameSessionSnapshot.builder(5)
.elapsedSeconds(12.75f)
.deaths(2)
.respawnSeconds(0.4f)
.player(new GameSessionSnapshot.PlayerData(
640f, 780f, 120f, -40f, false, false,
0.1f, 0.08f, -1, -1))
.addBody(new GameSessionSnapshot.BodyData(
500f, 826f, 72f, 54f, 0f, 0f,
false, false, true, true, -1, "SPIKE"))
.addBody(new GameSessionSnapshot.BodyData(
720f, 826f, 72f, 54f, 0f, 0f,
false, true, true, true, -1, "ELECTRIC"))
.platformMotionTimes(Arrays.asList(1.25f, 3.5f))
.toggleSignal("gate", true)
.timedSignal("clock", 2.25f)
.previousSwitchContact("gate@420.0", true)
.build();
}
}
@@ -0,0 +1,87 @@
package com.projectsacrifice.game;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public final class VolunteerRigTest {
@Test
public void skinsReuseTheSameNamedSkeletonAndOnlyChangePalette() {
VolunteerRig.Pose brass = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.IDLE, 0f, 1);
VolunteerRig.Pose verdigris = VolunteerRig.pose(
VolunteerRig.Skin.VERDIGRIS_KEEPER, VolunteerRig.Motion.IDLE, 0f, 1);
assertEquals(VolunteerRig.Joint.values().length, brass.jointCount());
for (VolunteerRig.Joint joint : VolunteerRig.Joint.values()) {
assertEquals(brass.x(joint), verdigris.x(joint), 0f);
assertEquals(brass.y(joint), verdigris.y(joint), 0f);
assertTrue(brass.x(joint) >= 0f && brass.x(joint) <= 1f);
assertTrue(brass.y(joint) >= 0f && brass.y(joint) <= 1f);
}
assertNotEquals(brass.palette().shell(), verdigris.palette().shell());
assertNotEquals(brass.palette().cloth(), verdigris.palette().cloth());
}
@Test
public void motionChangesLimbsWithoutChangingCollisionOrRootContract() {
VolunteerRig.Pose idle = VolunteerRig.pose(
VolunteerRig.Skin.EMBER_COURIER, VolunteerRig.Motion.IDLE, 0f, 1);
VolunteerRig.Pose run = VolunteerRig.pose(
VolunteerRig.Skin.EMBER_COURIER, VolunteerRig.Motion.RUN, 0.25f, 1);
VolunteerRig.Pose mirrored = VolunteerRig.pose(
VolunteerRig.Skin.EMBER_COURIER, VolunteerRig.Motion.RUN, 0.25f, -1);
assertEquals(62f, idle.collisionWidth(), 0f);
assertEquals(100f, idle.collisionHeight(), 0f);
assertEquals(idle.x(VolunteerRig.Joint.ROOT), run.x(VolunteerRig.Joint.ROOT), 0f);
assertNotEquals(idle.x(VolunteerRig.Joint.LEFT_HAND),
run.x(VolunteerRig.Joint.LEFT_HAND), 0.001f);
assertEquals(1f - run.x(VolunteerRig.Joint.LEFT_HAND),
mirrored.x(VolunteerRig.Joint.LEFT_HAND), 0.001f);
}
@Test
public void gameplayStatesHaveNonColorVisualSignals() {
VolunteerRig.Pose dead = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.DEAD, 0f, 1);
VolunteerRig.Pose frozen = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.FROZEN, 0f, 1);
VolunteerRig.Pose conductive = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.CONDUCTIVE, 0f, 1);
VolunteerRig.Pose pinned = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.PINNED, 0f, 1);
VolunteerRig.Pose respawn = VolunteerRig.pose(
VolunteerRig.Skin.BRASS_SCOUT, VolunteerRig.Motion.RESPAWN, 0.5f, 1);
assertFalse(dead.visorLit());
assertTrue(frozen.frozenShell());
assertTrue(conductive.conductiveArcs());
assertTrue(pinned.anchorBolt());
assertEquals(0.5f, respawn.assemblyProgress(), 0f);
}
@Test
public void gameplayMotionIsDerivedFromGroundVelocityAndBodyState() {
assertEquals(VolunteerRig.Motion.IDLE,
VolunteerRig.aliveMotion(true, 0f, 0f));
assertEquals(VolunteerRig.Motion.RUN,
VolunteerRig.aliveMotion(true, 370f, 0f));
assertEquals(VolunteerRig.Motion.JUMP,
VolunteerRig.aliveMotion(false, 120f, -400f));
assertEquals(VolunteerRig.Motion.FALL,
VolunteerRig.aliveMotion(false, 120f, 200f));
assertEquals(VolunteerRig.Motion.FROZEN,
VolunteerRig.bodyMotion(true, true, true));
assertEquals(VolunteerRig.Motion.CONDUCTIVE,
VolunteerRig.bodyMotion(false, true, true));
assertEquals(VolunteerRig.Motion.PINNED,
VolunteerRig.bodyMotion(false, false, true));
assertEquals(VolunteerRig.Motion.DEAD,
VolunteerRig.bodyMotion(false, false, false));
}
}