diff --git a/tests/Chip8.Tests/QuirksTests.cs b/tests/Chip8.Tests/QuirksTests.cs new file mode 100644 index 0000000..09aa86f --- /dev/null +++ b/tests/Chip8.Tests/QuirksTests.cs @@ -0,0 +1,104 @@ +using Chip8.Core; +using Xunit; + +namespace Chip8.Tests; + +public class QuirksTests +{ + static Machine drawRowAt(int originX, Quirks quirks) + { + var machine = new Machine(quirks, 1); + machine.memory[0x200] = 0xD0; + machine.memory[0x201] = 0x11; + machine.i = 0x300; + machine.memory[0x300] = 0xFF; + machine.v[0] = (byte)originX; + machine.v[1] = 0; + machine.step(); + return machine; + } + + [Fact] + public void clipQuirkTruncatesSpriteAtRightEdge() + { + var clipping = new Quirks { clipSprites = true }; + var machine = drawRowAt(62, clipping); + Assert.Equal(2, AsciiScreen.litPixelCount(machine.display)); + Assert.True(machine.display[62]); + Assert.True(machine.display[63]); + Assert.False(machine.display[0]); + } + + [Fact] + public void wrapQuirkWrapsSpriteAroundRightEdge() + { + var wrapping = new Quirks { clipSprites = false }; + var machine = drawRowAt(62, wrapping); + Assert.Equal(8, AsciiScreen.litPixelCount(machine.display)); + Assert.True(machine.display[62]); + Assert.True(machine.display[63]); + Assert.True(machine.display[0]); + Assert.True(machine.display[5]); + Assert.False(machine.display[6]); + } + + [Fact] + public void shiftQuirkSelectsSourceRegister() + { + var usesVx = TestHarness.withOpcode(0x8016, new Quirks { shiftUsesVy = false }); + usesVx.v[0] = 0b0000_0100; + usesVx.v[1] = 0xFF; + usesVx.step(); + Assert.Equal(0b0000_0010, usesVx.v[0]); + + var usesVy = TestHarness.withOpcode(0x8016, new Quirks { shiftUsesVy = true }); + usesVy.v[0] = 0x00; + usesVy.v[1] = 0b0000_0100; + usesVy.step(); + Assert.Equal(0b0000_0010, usesVy.v[0]); + } + + [Fact] + public void loadStoreIncrementQuirkAdvancesIndex() + { + var noIncrement = TestHarness.withOpcode(0xF255, new Quirks { loadStoreIncrementsI = false }); + noIncrement.i = 0x300; + noIncrement.step(); + Assert.Equal(0x300, noIncrement.i); + + var increment = TestHarness.withOpcode(0xF255, new Quirks { loadStoreIncrementsI = true }); + increment.i = 0x300; + increment.step(); + Assert.Equal(0x303, increment.i); + } + + [Fact] + public void jumpQuirkSelectsOffsetRegister() + { + var withV0 = TestHarness.withOpcode(0xB300, new Quirks { jumpWithVx = false }); + withV0.v[0] = 0x10; + withV0.v[3] = 0x20; + withV0.step(); + Assert.Equal(0x310, withV0.pc); + + var withVx = TestHarness.withOpcode(0xB300, new Quirks { jumpWithVx = true }); + withVx.v[0] = 0x10; + withVx.v[3] = 0x20; + withVx.step(); + Assert.Equal(0x320, withVx.pc); + } + + [Fact] + public void vfResetQuirkClearsFlagOnLogic() + { + var noReset = TestHarness.withOpcode(0x8011, new Quirks { resetVfOnLogic = false }); + noReset.v[0xF] = 0x5; + noReset.step(); + Assert.Equal(0x5, noReset.v[0xF]); + + var reset = TestHarness.withOpcode(0x8011, new Quirks { resetVfOnLogic = true }); + reset.v[0xF] = 0x5; + reset.step(); + Assert.Equal(0, reset.v[0xF]); + } +} diff --git a/tests/Chip8.Tests/SaveStateTests.cs b/tests/Chip8.Tests/SaveStateTests.cs new file mode 100644 index 0000000..040fc8d --- /dev/null +++ b/tests/Chip8.Tests/SaveStateTests.cs @@ -0,0 +1,104 @@ +using System.IO; +using Chip8.Core; +using Xunit; + +namespace Chip8.Tests; + +public class SaveStateTests +{ + [Fact] + public void loadStateRestoresRegistersMemoryAndDisplay() + { + var machine = TestHarness.withProgram(Platform.Chip8, 0x7001, 0x1200); + machine.step(); + machine.v[3] = 0xAB; + machine.i = 0x321; + machine.memory[0x400] = 0x42; + machine.display[5] = true; + + byte[] state = machine.saveState(); + + machine.v[3] = 0x00; + machine.i = 0x000; + machine.memory[0x400] = 0x99; + machine.display[5] = false; + machine.pc = 0x2FF; + + machine.loadState(state); + + Assert.Equal(0xAB, machine.v[3]); + Assert.Equal(0x321, machine.i); + Assert.Equal(0x42, machine.memory[0x400]); + Assert.True(machine.display[5]); + Assert.Equal(0x202, machine.pc); + } + + [Fact] + public void restoreBuildsANewMachineFromState() + { + var machine = TestHarness.withProgram(Platform.SuperChip, 0x6009); + machine.step(); + byte[] state = machine.saveState(); + + Machine restored = Machine.restore(state); + + Assert.Equal(Platform.SuperChip, restored.platform); + Assert.Equal(machine.v[0], restored.v[0]); + Assert.Equal(machine.pc, restored.pc); + } + + [Fact] + public void restoredMachineExecutesIdenticallyToTheOriginal() + { + var original = TestHarness.withProgram(Platform.Chip8, 0x7001, 0x1200); + for (int step = 0; step < 20; step++) + { + original.step(); + } + + Machine resumed = Machine.restore(original.saveState()); + for (int step = 0; step < 15; step++) + { + original.step(); + resumed.step(); + } + + Assert.Equal(original.pc, resumed.pc); + Assert.Equal(original.v[0], resumed.v[0]); + Assert.Equal(original.i, resumed.i); + } + + [Fact] + public void xoChipStatePreservesPlanesAndAudio() + { + var machine = TestHarness.withProgram(Platform.XoChip, 0xF201, 0xA400, 0xD001); + machine.memory[0x400] = 0x80; + machine.audioPattern[3] = 0xC3; + for (int step = 0; step < 3; step++) + { + machine.step(); + } + Assert.Equal(2, machine.video.colorAt(0, 0)); + + Machine restored = Machine.restore(machine.saveState()); + + Assert.Equal(2, restored.planeMask); + Assert.Equal(2, restored.video.colorAt(0, 0)); + Assert.Equal(0xC3, restored.audioPattern[3]); + } + + [Fact] + public void loadStateRejectsAMismatchedPlatform() + { + var chip8 = new Machine(Quirks.modern(), 1); + byte[] state = chip8.saveState(); + var xoChip = new Machine(null, 1, Platform.XoChip); + Assert.Throws(() => xoChip.loadState(state)); + } + + [Fact] + public void restoreRejectsGarbage() + { + Assert.Throws(() => Machine.restore(new byte[] { 1, 2, 3, 4, 5 })); + } +}