using VPlasma.Core.Device; using Xunit; namespace VPlasma.Core.Tests; public class VPlasmaDeviceTests { private const byte Esc = 0x1B; private static void Feed(VPlasmaDevice device, params byte[] bytes) => device.OnReceived(bytes, bytes.Length); private static void Feed(VPlasmaDevice device, IEnumerable bytes) { byte[] arr = bytes.ToArray(); device.OnReceived(arr, arr.Length); } private static byte Pixel(VPlasmaDevice device, int x, int y) { var frame = new byte[VPlasmaDevice.Width * VPlasmaDevice.Height]; device.CopyFrame(frame); return frame[y * VPlasmaDevice.Width + x]; } /// A full-width ESC P row the way L4PLASMA.CPP sends one. private static byte[] GraphicsRow(int y, params byte[] data) { var row = new List { Esc, (byte)'P', 0, (byte)y, 0, (byte)data.Length, 1 }; row.AddRange(data); return row.ToArray(); } // ---- graphics writes ------------------------------------------------- [Fact] public void GraphicsRow_SetsPixelsMsbFirst() { var device = new VPlasmaDevice(); Feed(device, GraphicsRow(5, 0x80, 0x01)); // xbyte 0..1 Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 5)); // MSB of byte 0 Assert.Equal(0, Pixel(device, 1, 5)); Assert.Equal(0, Pixel(device, 14, 5)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 15, 5)); // LSB of byte 1 Assert.Equal(0, Pixel(device, 0, 4)); Assert.Equal(0, Pixel(device, 0, 6)); Assert.Equal(1, device.GraphicsRows); } [Fact] public void GraphicsRow_SurvivesAnyChunkBoundary() { byte[] wire = GraphicsRow(3, Enumerable.Repeat((byte)0xFF, 16).ToArray()); for (int split = 1; split < wire.Length; ++split) { var device = new VPlasmaDevice(); device.OnReceived(wire, split); byte[] rest = wire.Skip(split).ToArray(); device.OnReceived(rest, rest.Length); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 3)); Assert.Equal(1, device.GraphicsRows); } } [Fact] public void GraphicsBlock_MultipleRowsAdvanceY() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'P', 0, 10, 0, 1, 3, 0x80, 0x80, 0x80); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 10)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 11)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 12)); Assert.Equal(0, Pixel(device, 0, 13)); Assert.Equal(3, device.GraphicsRows); } [Fact] public void GraphicsWrite_HonorsByteColumnOffset() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'P', 0, 0, 2, 1, 1, 0xFF); // xbyte=2 → x 16..23 Assert.Equal(0, Pixel(device, 15, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 16, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 23, 0)); Assert.Equal(0, Pixel(device, 24, 0)); } [Fact] public void GraphicsWrite_OutOfRangeRowIsConsumedNotDrawn() { var device = new VPlasmaDevice(); Feed(device, GraphicsRow(40, Enumerable.Repeat((byte)0xFF, 16).ToArray())); Feed(device, (byte)'H'); // parser must be back in text mode Assert.Equal(1, device.TextCharsDrawn); Assert.Equal(0, device.GraphicsRows); } [Fact] public void GraphicsWrite_OverwritesTextAttributes() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'H', 8); // flashing text Feed(device, (byte)'H'); // font-0 'H' lights (0,0) Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelFlash, Pixel(device, 0, 0)); Feed(device, GraphicsRow(0, 0x80)); // repaint that row from the wire Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); } // ---- text mode (real font 0 = 6×8) ----------------------------------- [Fact] public void Text_DrawsRealGlyphAndAdvancesByFontWidth() { var device = new VPlasmaDevice(); Feed(device, (byte)'H'); // font-0 'H': row0 = pixels at x0 and x4 Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 0)); Assert.Equal(0, Pixel(device, 1, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 3)); // crossbar row Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 4, 3)); Assert.Equal(0, Pixel(device, 0, 7)); // blank bottom row Assert.Equal(6, device.CursorX); // advanced one 6-px cell Assert.Equal(0, device.CursorY); Assert.Equal(1, device.TextCharsDrawn); } [Fact] public void ControlChars_MoveCursorByPixels() { var device = new VPlasmaDevice(); // font 0: 6 wide, 8 tall Feed(device, 0x09, 0x09, 0x09); // HT ×3 → x = 18 Assert.Equal(18, device.CursorX); Feed(device, 0x08); // BS → x = 12 Assert.Equal(12, device.CursorX); Feed(device, 0x0A); // LF → y = 8 Assert.Equal(8, device.CursorY); Assert.Equal(0, device.CursorX); Feed(device, (byte)'H', 0x0D); // draw + CR → x back to 0 Assert.Equal(0, device.CursorX); Feed(device, 0x0B); // VT → y back to 0 Assert.Equal(0, device.CursorY); } [Fact] public void EscQ_EscR_SetPixelCursor() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'Q', 20); // set row Y=20 Feed(device, Esc, (byte)'R', 37); // set col X=37 Assert.Equal(37, device.CursorX); Assert.Equal(20, device.CursorY); Feed(device, Esc, (byte)'Q', 40); // out of range (>31): ignored Assert.Equal(20, device.CursorY); Feed(device, Esc, (byte)'R', 200); // out of range (>127): ignored Assert.Equal(37, device.CursorX); } [Fact] public void Text_WrapsAtRightAndBottom() { var device = new VPlasmaDevice(); // 21 cells of 6px per line, 4 lines Feed(device, Enumerable.Repeat((byte)'X', 21)); // fills first line, wraps Assert.Equal(0, device.CursorX); Assert.Equal(8, device.CursorY); Feed(device, Enumerable.Repeat((byte)'X', 21 * 3)); // to the end, wraps to top Assert.Equal(0, device.CursorX); Assert.Equal(0, device.CursorY); } [Fact] public void EscAt_ClearsScreenAndResetsTextState() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'K', 4, Esc, (byte)'H', 1, (byte)'H'); Feed(device, Esc, (byte)'@'); Assert.Equal(0, Pixel(device, 0, 0)); Assert.Equal(0, device.CursorX); Assert.Equal(0, device.CursorY); Assert.Equal(0, device.Font); Assert.Equal(PlasmaAttributes.None, device.Attributes); } [Fact] public void EscL_HomesCursorWithoutClearing() { var device = new VPlasmaDevice(); Feed(device, (byte)'H', Esc, (byte)'L'); Assert.Equal(0, device.CursorX); Assert.Equal(0, device.CursorY); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); // glyph survives } [Theory] [InlineData(0x00, PlasmaCursorMode.Hidden)] // the game's cursor-off [InlineData(0xFF, PlasmaCursorMode.Hidden)] // the test tool's hide [InlineData(0x01, PlasmaCursorMode.Steady)] [InlineData(0x03, PlasmaCursorMode.Flashing)] public void EscG_SetsCursorMode(byte operand, PlasmaCursorMode expected) { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'G', operand); Assert.Equal(expected, device.CursorMode); } [Fact] public void EscK_SelectsRealFonts() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'K', 4); // large font: 12×16 Assert.Equal(4, device.Font); Assert.Equal(12, device.FontWidth); Assert.Equal(16, device.FontHeight); Feed(device, Esc, (byte)'K', 0); // back to font 0: 6×8 Assert.Equal(0, device.Font); Assert.Equal(6, device.FontWidth); Assert.Equal(8, device.FontHeight); Feed(device, Esc, (byte)'K', 0xFF); // out of range: firmware ignores it Assert.Equal(0, device.Font); } [Fact] public void EscH_AppliesAttributesAsLowFourBits() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'H', 1, (byte)'H'); // half intensity Assert.Equal(VPlasmaDevice.PixelLit | VPlasmaDevice.PixelHalf, Pixel(device, 0, 0)); Feed(device, Esc, (byte)'L', Esc, (byte)'@'); // clear Feed(device, Esc, (byte)'H', 2, (byte)'H'); // underline: bottom row (y=7) lit Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 7)); Feed(device, Esc, (byte)'@'); Feed(device, Esc, (byte)'H', 4, (byte)' '); // reverse: a space renders solid Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 5, 7)); Feed(device, Esc, (byte)'H', 0); // defaults restored Assert.Equal(PlasmaAttributes.None, device.Attributes); } // ---- robustness ---------------------------------------------------------- [Fact] public void UnknownEscape_IsConsumedAndTextResumes() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'\\', (byte)'H'); // ESC '\' is not a command Assert.Equal(1, device.TextCharsDrawn); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); } [Fact] public void GameStartupSequence_HidesCursor() { var device = new VPlasmaDevice(); Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); // power-on default Feed(device, 27, (byte)'G', 0x00); // L4PLASMA.CPP's boot byte Assert.Equal(PlasmaCursorMode.Hidden, device.CursorMode); } [Fact] public void SelfTestPages_ParseWithoutUnknownCommands() { for (int page = 0; page < PlasmaSelfTest.PageCount; ++page) { var device = new VPlasmaDevice(); var complaints = new List(); device.Logged += line => { if (line.StartsWith("Unknown", StringComparison.Ordinal) || line.StartsWith("Unhandled", StringComparison.Ordinal)) complaints.Add(line); }; byte[] bytes = PlasmaSelfTest.BuildPage(page); device.OnReceived(bytes, bytes.Length); Assert.Empty(complaints); Assert.True(device.BytesReceived > 0); } } // ---- orientation + test pattern (JP1 jumpers 4 & 5) -------------------- [Fact] public void Orientation_VerticalRotatesLogicalOntoPhysicalGlass() { var device = new VPlasmaDevice { Orientation = PlasmaOrientation.Vertical }; Assert.Equal(32, device.LogicalWidth); Assert.Equal(128, device.LogicalHeight); // Logical (0,0) maps to physical (px=ly=0, py=Height-1-lx=31). Feed(device, Esc, (byte)'R', 0, Esc, (byte)'Q', 0, (byte)'H'); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 31)); // 'H' top-left dot Assert.Equal(0, Pixel(device, 0, 0)); // ESC Q (row Y) now accepts up to the 128-tall logical height; // ESC R (column X) is bounded by the 32-wide logical width. Feed(device, Esc, (byte)'Q', 100); Assert.Equal(100, device.CursorY); Feed(device, Esc, (byte)'R', 40); // out of range for 32-wide vertical Assert.NotEqual(40, device.CursorX); } [Fact] public void Orientation_ChangeClearsAndHomes() { var device = new VPlasmaDevice(); Feed(device, (byte)'H'); Assert.NotEqual(0, Pixel(device, 0, 0)); device.Orientation = PlasmaOrientation.Vertical; Assert.Equal(0, Pixel(device, 0, 0)); // cleared Assert.Equal(0, device.CursorX); Assert.Equal(0, device.CursorY); } [Fact] public void ShowTestPattern_LightsEveryDot() { var device = new VPlasmaDevice(); device.ShowTestPattern(); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 127, 31)); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 64, 16)); device.Reset(); Assert.Equal(0, Pixel(device, 64, 16)); } [Fact] public void FirmwareDemo_ReplaysAllScreensAndDrawsText() { Assert.Equal(10, PlasmaFirmwareDemo.Count); var device = new VPlasmaDevice(); foreach (byte[] screen in PlasmaFirmwareDemo.Screens) { Assert.NotEmpty(screen); device.OnReceived(screen, screen.Length); } // The 10-screen demo puts plenty of real text on the glass. Assert.True(device.TextCharsDrawn > 0); } [Fact] public void PageSelect_ConsumesItsOperand() { var device = new VPlasmaDevice(); // ESC I 2 (draw page) must swallow the '2'; then 'H' is the first glyph. Feed(device, Esc, (byte)'I', 2, Esc, (byte)'i', 3, (byte)'H'); Assert.Equal(1, device.TextCharsDrawn); Assert.Equal(VPlasmaDevice.PixelLit, Pixel(device, 0, 0)); Assert.Equal(6, device.CursorX); // advanced exactly one cell (no stray chars) } [Fact] public void Reset_RestoresPowerOnState() { var device = new VPlasmaDevice(); Feed(device, Esc, (byte)'K', 5, Esc, (byte)'H', 4, Esc, (byte)'G', 0, (byte)'H'); device.Reset(); Assert.Equal(0, Pixel(device, 0, 0)); Assert.Equal(0, device.Font); Assert.Equal(PlasmaAttributes.None, device.Attributes); Assert.Equal(PlasmaCursorMode.Steady, device.CursorMode); Assert.Equal(0, device.CursorX); Assert.Equal(0, device.CursorY); } }