Завершить инженерный кандидат Stage 5
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user