ConsoleGamesWASM
/
BlazorConsole.cs
581 строка · 13.8 Кб
1using Microsoft.AspNetCore.Components;2using Microsoft.AspNetCore.Components.Web;3using System;4using System.Collections.Generic;5using System.Text;6using System.Threading.Tasks;7using System.Web;8using Towel;9
10namespace Website;11
12public class BlazorConsole13{
14public struct Pixel15{16public char Char;17public ConsoleColor BackgroundColor;18public ConsoleColor ForegroundColor;19
20public static bool operator ==(Pixel a, Pixel b) =>21a.Char == b.Char &&22a.ForegroundColor == b.ForegroundColor &&23a.BackgroundColor == b.BackgroundColor;24
25public static bool operator !=(Pixel a, Pixel b) => !(a == b);26
27public override readonly bool Equals(object? obj) => obj is Pixel pixel && this == pixel;28
29public override readonly int GetHashCode() => HashCode.Combine(Char, ForegroundColor, BackgroundColor);30}31
32#pragma warning disable CA2211 // Non-constant fields should not be visible33public static BlazorConsole? ActiveConsole;34#pragma warning restore CA2211 // Non-constant fields should not be visible35
36public const int Delay = 1; // milliseconds37public const int InactiveDelay = 1000; // milliseconds38public readonly Queue<ConsoleKeyInfo> InputBuffer = new();39public Action? TriggerRefresh;40public bool RefreshOnInputOnly = true;41public Pixel[,] View;42public bool StateHasChanged = true;43
44public string? Title;45public ConsoleColor BackgroundColor = ConsoleColor.Black;46public ConsoleColor ForegroundColor = ConsoleColor.White;47public bool _cursorVisible = true;48public int LargestWindowWidth = 120;49public int LargestWindowHeight = 51;50
51public int _windowHeight = 35;52public int _windowWidth = 80;53public int _cursorLeft = 0;54public int _cursorTop = 0;55
56public Encoding? OutputEncoding;57
58public bool CursorVisible59{60get => _cursorVisible;61set62{63_cursorVisible = value;64StateHasChanged = true;65}66}67
68public int CursorLeft69{70get => _cursorLeft;71set72{73_cursorLeft = value;74StateHasChanged = true;75}76}77
78public int CursorTop79{80get => _cursorTop;81set82{83_cursorTop = value;84StateHasChanged = true;85}86}87
88public int WindowHeight89{90get => _windowHeight;91set92{93_windowHeight = value;94HandleResize();95}96}97
98public int WindowWidth99{100get => _windowWidth;101set102{103_windowWidth = value;104HandleResize();105}106}107
108public int BufferWidth109{110get => WindowWidth;111set => WindowWidth = value;112}113
114public int BufferHeight115{116get => WindowHeight;117set => WindowHeight = value;118}119
120#pragma warning disable CA1822 // Mark members as static121#pragma warning disable IDE0060 // Remove unused parameter122
123public void SetWindowPosition(int left, int top)124{125// do nothing :)126}127
128#pragma warning restore IDE0060 // Remove unused parameter129#pragma warning restore CA1822 // Mark members as static130
131public void SetWindowSize(int width, int height)132{133WindowWidth = width;134WindowHeight = height;135}136
137public void SetBufferSize(int width, int height) => SetWindowSize(width, height);138
139public void EnqueueInput(ConsoleKey key, bool shift = false, bool alt = false, bool control = false)140{141char c = key switch142{143>= ConsoleKey.A and <= ConsoleKey.Z => (char)(key - ConsoleKey.A + 'a'),144>= ConsoleKey.D0 and <= ConsoleKey.D9 => (char)(key - ConsoleKey.D0 + '0'),145ConsoleKey.Enter => '\n',146ConsoleKey.Backspace => '\b',147ConsoleKey.OemPeriod => '.',148ConsoleKey.OemMinus => '-',149_ => '\0',150};151InputBuffer.Enqueue(new(shift ? char.ToUpper(c) : c, key, shift, alt, control));152}153
154public void OnKeyDown(KeyboardEventArgs e)155{156switch (e.Key)157{158case "Home": EnqueueInput(ConsoleKey.Home); break;159case "End": EnqueueInput(ConsoleKey.End); break;160case "Backspace": EnqueueInput(ConsoleKey.Backspace); break;161case " ": EnqueueInput(ConsoleKey.Spacebar); break;162case "Delete": EnqueueInput(ConsoleKey.Delete); break;163case "Enter": EnqueueInput(ConsoleKey.Enter); break;164case "Escape": EnqueueInput(ConsoleKey.Escape); break;165case "ArrowLeft": EnqueueInput(ConsoleKey.LeftArrow); break;166case "ArrowRight": EnqueueInput(ConsoleKey.RightArrow); break;167case "ArrowUp": EnqueueInput(ConsoleKey.UpArrow); break;168case "ArrowDown": EnqueueInput(ConsoleKey.DownArrow); break;169case ".": EnqueueInput(ConsoleKey.OemPeriod); break;170case "-": EnqueueInput(ConsoleKey.OemMinus); break;171default:172if (e.Key.Length is 1)173{174char c = e.Key[0];175switch (c)176{177case >= '0' and <= '9': EnqueueInput(ConsoleKey.D0 + (c - '0')); break;178case >= 'a' and <= 'z': EnqueueInput(ConsoleKey.A + (c - 'a')); break;179case >= 'A' and <= 'Z': EnqueueInput(ConsoleKey.A + (c - 'A'), shift: true); break;180}181}182break;183}184}185
186public static string HtmlEncode(ConsoleColor color)187{188return color switch189{190ConsoleColor.Black => "#000000",191ConsoleColor.White => "#ffffff",192ConsoleColor.Blue => "#0000ff",193ConsoleColor.Red => "#ff0000",194ConsoleColor.Green => "#00ff00",195ConsoleColor.Yellow => "#ffff00",196ConsoleColor.Cyan => "#00ffff",197ConsoleColor.Magenta => "#ff00ff",198ConsoleColor.Gray => "#808080",199ConsoleColor.DarkBlue => "#00008b",200ConsoleColor.DarkRed => "#8b0000",201ConsoleColor.DarkGreen => "#006400",202ConsoleColor.DarkYellow => "#8b8000",203ConsoleColor.DarkCyan => "#008b8b",204ConsoleColor.DarkMagenta => "#8b008b",205ConsoleColor.DarkGray => "#a9a9a9",206_ => throw new NotImplementedException(),207};208}209
210public void ResetColor()211{212BackgroundColor = ConsoleColor.Black;213ForegroundColor = ConsoleColor.White;214}215
216public BlazorConsole()217{218ActiveConsole = this;219View = new Pixel[WindowHeight, WindowWidth];220ClearNoRefresh();221}222
223public void DieIfNotActiveGame()224{225if (this != ActiveConsole)226{227throw new Exception("die :P pew pew");228}229}230
231public async Task RefreshAndDelay(TimeSpan timeSpan)232{233DieIfNotActiveGame();234if (StateHasChanged)235{236TriggerRefresh?.Invoke();237}238await Task.Delay(timeSpan);239}240
241public void HandleResize()242{243if (View.GetLength(0) != WindowHeight || View.GetLength(1) != WindowWidth)244{245Pixel[,] old_view = View;246View = new Pixel[WindowHeight, WindowWidth];247for (int row = 0; row < View.GetLength(0) && row < old_view.GetLength(0); row++)248{249for (int column = 0; column < View.GetLength(1) && column < old_view.GetLength(1); column++)250{251View[row, column] = old_view[row, column];252}253}254StateHasChanged = true;255}256}257
258public async Task Refresh()259{260DieIfNotActiveGame();261if (StateHasChanged)262{263TriggerRefresh?.Invoke();264}265await Task.Delay(Delay);266}267
268public MarkupString State269{270get271{272StringBuilder stateBuilder = new();273for (int row = 0; row < View.GetLength(0); row++)274{275for (int column = 0; column < View.GetLength(1); column++)276{277if (CursorVisible && (CursorLeft, CursorTop) == (column, row))278{279bool isDark =280(View[row, column].Char is '█' && View[row, column].ForegroundColor is ConsoleColor.White) ||281(View[row, column].Char is ' ' && View[row, column].BackgroundColor is ConsoleColor.White);282stateBuilder.Append($@"<span class=""cursor {(isDark ? "cursor-dark" : "cursor-light")}"">");283}284if (View[row, column].BackgroundColor is not ConsoleColor.Black)285{286stateBuilder.Append($@"<span style=""background-color:{HtmlEncode(View[row, column].BackgroundColor)}"">");287}288if (View[row, column].ForegroundColor is not ConsoleColor.White)289{290stateBuilder.Append($@"<span style=""color:{HtmlEncode(View[row, column].ForegroundColor)}"">");291}292stateBuilder.Append(HttpUtility.HtmlEncode(View[row, column].Char));293if (View[row, column].ForegroundColor is not ConsoleColor.White)294{295stateBuilder.Append("</span>");296}297if (View[row, column].BackgroundColor is not ConsoleColor.Black)298{299stateBuilder.Append("</span>");300}301if (CursorVisible && (CursorLeft, CursorTop) == (column, row))302{303stateBuilder.Append("</span>");304}305}306stateBuilder.Append("<br />");307}308string state = stateBuilder.ToString();309StateHasChanged = false;310return (MarkupString)state;311}312}313
314public void ResetColors()315{316DieIfNotActiveGame();317BackgroundColor = ConsoleColor.Black;318ForegroundColor = ConsoleColor.White;319}320
321public async Task Clear()322{323DieIfNotActiveGame();324ClearNoRefresh();325if (!RefreshOnInputOnly)326{327await Refresh();328}329}330
331public void ClearNoRefresh()332{333DieIfNotActiveGame();334for (int row = 0; row < View.GetLength(0); row++)335{336for (int column = 0; column < View.GetLength(1); column++)337{338Pixel pixel = new()339{340Char = ' ',341BackgroundColor = BackgroundColor,342ForegroundColor = ForegroundColor,343};344StateHasChanged = StateHasChanged || pixel != View[row, column];345View[row, column] = pixel;346}347}348(CursorLeft, CursorTop) = (0, 0);349}350
351public void WriteNoRefresh(char c)352{353DieIfNotActiveGame();354if (c is '\r')355{356return;357}358if (c is '\n')359{360WriteLineNoRefresh();361return;362}363if (CursorLeft >= View.GetLength(1))364{365(CursorLeft, CursorTop) = (0, CursorTop + 1);366}367if (CursorTop >= View.GetLength(0))368{369for (int row = 0; row < View.GetLength(0) - 1; row++)370{371for (int column = 0; column < View.GetLength(1); column++)372{373StateHasChanged = StateHasChanged || View[row, column] != View[row + 1, column];374View[row, column] = View[row + 1, column];375}376}377for (int column = 0; column < View.GetLength(1); column++)378{379Pixel pixel = new()380{381Char = ' ',382BackgroundColor = BackgroundColor,383ForegroundColor = ForegroundColor384};385StateHasChanged = StateHasChanged || View[View.GetLength(0) - 1, column] != pixel;386View[View.GetLength(0) - 1, column] = pixel;387}388CursorTop--;389}390{391Pixel pixel = new()392{393Char = c,394BackgroundColor = BackgroundColor,395ForegroundColor = ForegroundColor396};397StateHasChanged = StateHasChanged || View[CursorTop, CursorLeft] != pixel;398View[CursorTop, CursorLeft] = pixel;399}400CursorLeft++;401}402
403public void WriteLineNoRefresh()404{405DieIfNotActiveGame();406while (CursorLeft < View.GetLength(1))407{408WriteNoRefresh(' ');409}410(CursorLeft, CursorTop) = (0, CursorTop + 1);411}412
413public async Task Write(object o)414{415DieIfNotActiveGame();416if (o is null) return;417string? s = o.ToString();418if (s is null || s is "") return;419foreach (char c in s)420{421WriteNoRefresh(c);422}423if (!RefreshOnInputOnly)424{425await Refresh();426}427}428
429public async Task WriteLine()430{431WriteLineNoRefresh();432await Refresh();433}434
435public async Task WriteLine(object o)436{437if (o is not null)438{439string? s = o.ToString();440if (s is not null)441{442foreach (char c in s)443{444WriteNoRefresh(c);445}446}447}448WriteLineNoRefresh();449if (!RefreshOnInputOnly)450{451await Refresh();452}453}454
455public ConsoleKeyInfo ReadKeyNoRefresh(bool capture)456{457if (!KeyAvailableNoRefresh())458{459throw new InvalidOperationException("attempting a no refresh ReadKey with an empty input buffer");460}461var keyInfo = InputBuffer.Dequeue();462if (capture is false)463{464switch (keyInfo.KeyChar)465{466case '\n': WriteLineNoRefresh(); break;467case '\0': break;468case '\b': throw new NotImplementedException("ReadKey backspace not implemented");469default: WriteNoRefresh(keyInfo.KeyChar); break;470}471}472return keyInfo;473}474
475public async Task<ConsoleKeyInfo> ReadKey(bool capture)476{477while (!KeyAvailableNoRefresh())478{479await Refresh();480}481return ReadKeyNoRefresh(capture);482}483
484public async Task<string> ReadLine(bool v)485{486string line = string.Empty;487while (true)488{489while (!KeyAvailableNoRefresh())490{491await Refresh();492}493var keyInfo = InputBuffer.Dequeue();494switch (keyInfo.Key)495{496case ConsoleKey.Backspace:497if (line.Length > 0)498{499if (CursorLeft > 0)500{501CursorLeft--;502StateHasChanged = true;503View[CursorTop, CursorLeft].Char = ' ';504}505line = line[..^1];506await Refresh();507}508break;509case ConsoleKey.Enter:510WriteLineNoRefresh();511await Refresh();512return line;513default:514if (keyInfo.KeyChar is not '\0')515{516line += keyInfo.KeyChar;517WriteNoRefresh(keyInfo.KeyChar);518await Refresh();519}520break;521}522}523}524
525public bool KeyAvailableNoRefresh()526{527return InputBuffer.Count > 0;528}529
530public async Task<bool> KeyAvailable()531{532await Refresh();533return KeyAvailableNoRefresh();534}535
536public async Task SetCursorPosition(int left, int top)537{538(CursorLeft, CursorTop) = (left, top);539if (!RefreshOnInputOnly)540{541await Refresh();542}543}544
545public async Task PromptPressToContinue(string? prompt = null, ConsoleKey key = ConsoleKey.Enter)546{547if (!key.IsDefined())548{549throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(ConsoleKey)} enum");550}551prompt ??= $"Press [{key}] to continue...";552foreach (char c in prompt)553{554WriteNoRefresh(c);555}556await PressToContinue(key);557}558
559public async Task PressToContinue(ConsoleKey key = ConsoleKey.Enter)560{561if (!key.IsDefined())562{563throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(ConsoleKey)} enum");564}565while ((await ReadKey(true)).Key != key)566{567continue;568}569}570
571#pragma warning disable CA1822 // Mark members as static572/// <summary>573/// Returns true. Some members of <see cref="Console"/> only work574/// on Windows such as <see cref="Console.WindowWidth"/>, but even though this575/// is blazor and not necessarily on Windows, this wrapper contains implementations576/// for those Windows-only members.577/// </summary>578/// <returns>true</returns>579public bool IsWindows() => true;580#pragma warning restore CA1822 // Mark members as static581}