ConsoleGamesWASM

Форк
0
/
BlazorConsole.cs 
581 строка · 13.8 Кб
1
using Microsoft.AspNetCore.Components;
2
using Microsoft.AspNetCore.Components.Web;
3
using System;
4
using System.Collections.Generic;
5
using System.Text;
6
using System.Threading.Tasks;
7
using System.Web;
8
using Towel;
9

10
namespace Website;
11

12
public class BlazorConsole
13
{
14
	public struct Pixel
15
	{
16
		public char Char;
17
		public ConsoleColor BackgroundColor;
18
		public ConsoleColor ForegroundColor;
19

20
		public static bool operator ==(Pixel a, Pixel b) =>
21
			a.Char == b.Char &&
22
			a.ForegroundColor == b.ForegroundColor &&
23
			a.BackgroundColor == b.BackgroundColor;
24

25
		public static bool operator !=(Pixel a, Pixel b) => !(a == b);
26

27
		public override readonly bool Equals(object? obj) => obj is Pixel pixel && this == pixel;
28

29
		public override readonly int GetHashCode() => HashCode.Combine(Char, ForegroundColor, BackgroundColor);
30
	}
31

32
#pragma warning disable CA2211 // Non-constant fields should not be visible
33
	public static BlazorConsole? ActiveConsole;
34
#pragma warning restore CA2211 // Non-constant fields should not be visible
35

36
	public const int Delay = 1; // milliseconds
37
	public const int InactiveDelay = 1000; // milliseconds
38
	public readonly Queue<ConsoleKeyInfo> InputBuffer = new();
39
	public Action? TriggerRefresh;
40
	public bool RefreshOnInputOnly = true;
41
	public Pixel[,] View;
42
	public bool StateHasChanged = true;
43

44
	public string? Title;
45
	public ConsoleColor BackgroundColor = ConsoleColor.Black;
46
	public ConsoleColor ForegroundColor = ConsoleColor.White;
47
	public bool _cursorVisible = true;
48
	public int LargestWindowWidth = 120;
49
	public int LargestWindowHeight = 51;
50

51
	public int _windowHeight = 35;
52
	public int _windowWidth = 80;
53
	public int _cursorLeft = 0;
54
	public int _cursorTop = 0;
55

56
	public Encoding? OutputEncoding;
57

58
	public bool CursorVisible
59
	{
60
		get => _cursorVisible;
61
		set
62
		{
63
			_cursorVisible = value;
64
			StateHasChanged = true;
65
		}
66
	}
67

68
	public int CursorLeft
69
	{
70
		get => _cursorLeft;
71
		set
72
		{
73
			_cursorLeft = value;
74
			StateHasChanged = true;
75
		}
76
	}
77

78
	public int CursorTop
79
	{
80
		get => _cursorTop;
81
		set
82
		{
83
			_cursorTop = value;
84
			StateHasChanged = true;
85
		}
86
	}
87

88
	public int WindowHeight
89
	{
90
		get => _windowHeight;
91
		set
92
		{
93
			_windowHeight = value;
94
			HandleResize();
95
		}
96
	}
97

98
	public int WindowWidth
99
	{
100
		get => _windowWidth;
101
		set
102
		{
103
			_windowWidth = value;
104
			HandleResize();
105
		}
106
	}
107

108
	public int BufferWidth
109
	{
110
		get => WindowWidth;
111
		set => WindowWidth = value;
112
	}
113

114
	public int BufferHeight
115
	{
116
		get => WindowHeight;
117
		set => WindowHeight = value;
118
	}
119

120
#pragma warning disable CA1822 // Mark members as static
121
#pragma warning disable IDE0060 // Remove unused parameter
122

123
	public void SetWindowPosition(int left, int top)
124
	{
125
		// do nothing :)
126
	}
127

128
#pragma warning restore IDE0060 // Remove unused parameter
129
#pragma warning restore CA1822 // Mark members as static
130

131
	public void SetWindowSize(int width, int height)
132
	{
133
		WindowWidth = width;
134
		WindowHeight = height;
135
	}
136

137
	public void SetBufferSize(int width, int height) => SetWindowSize(width, height);
138

139
	public void EnqueueInput(ConsoleKey key, bool shift = false, bool alt = false, bool control = false)
140
	{
141
		char c = key switch
142
		{
143
			>= ConsoleKey.A and <= ConsoleKey.Z => (char)(key - ConsoleKey.A + 'a'),
144
			>= ConsoleKey.D0 and <= ConsoleKey.D9 => (char)(key - ConsoleKey.D0 + '0'),
145
			ConsoleKey.Enter => '\n',
146
			ConsoleKey.Backspace => '\b',
147
			ConsoleKey.OemPeriod => '.',
148
			ConsoleKey.OemMinus => '-',
149
			_ => '\0',
150
		};
151
		InputBuffer.Enqueue(new(shift ? char.ToUpper(c) : c, key, shift, alt, control));
152
	}
153

154
	public void OnKeyDown(KeyboardEventArgs e)
155
	{
156
		switch (e.Key)
157
		{
158
			case "Home":       EnqueueInput(ConsoleKey.Home); break;
159
			case "End":        EnqueueInput(ConsoleKey.End); break;
160
			case "Backspace":  EnqueueInput(ConsoleKey.Backspace); break;
161
			case " ":          EnqueueInput(ConsoleKey.Spacebar); break;
162
			case "Delete":     EnqueueInput(ConsoleKey.Delete); break;
163
			case "Enter":      EnqueueInput(ConsoleKey.Enter); break;
164
			case "Escape":     EnqueueInput(ConsoleKey.Escape); break;
165
			case "ArrowLeft":  EnqueueInput(ConsoleKey.LeftArrow); break;
166
			case "ArrowRight": EnqueueInput(ConsoleKey.RightArrow); break;
167
			case "ArrowUp":    EnqueueInput(ConsoleKey.UpArrow); break;
168
			case "ArrowDown":  EnqueueInput(ConsoleKey.DownArrow); break;
169
			case ".":          EnqueueInput(ConsoleKey.OemPeriod); break;
170
			case "-":          EnqueueInput(ConsoleKey.OemMinus); break;
171
			default:
172
				if (e.Key.Length is 1)
173
				{
174
					char c = e.Key[0];
175
					switch (c)
176
					{
177
						case >= '0' and <= '9': EnqueueInput(ConsoleKey.D0 + (c - '0'));              break;
178
						case >= 'a' and <= 'z': EnqueueInput(ConsoleKey.A  + (c - 'a'));              break;
179
						case >= 'A' and <= 'Z': EnqueueInput(ConsoleKey.A  + (c - 'A'), shift: true); break;
180
					}
181
				}
182
				break;
183
		}
184
	}
185

186
	public static string HtmlEncode(ConsoleColor color)
187
	{
188
		return color switch
189
		{
190
			ConsoleColor.Black =>       "#000000",
191
			ConsoleColor.White =>       "#ffffff",
192
			ConsoleColor.Blue =>        "#0000ff",
193
			ConsoleColor.Red =>         "#ff0000",
194
			ConsoleColor.Green =>       "#00ff00",
195
			ConsoleColor.Yellow =>      "#ffff00",
196
			ConsoleColor.Cyan =>        "#00ffff",
197
			ConsoleColor.Magenta =>     "#ff00ff",
198
			ConsoleColor.Gray =>        "#808080",
199
			ConsoleColor.DarkBlue =>    "#00008b",
200
			ConsoleColor.DarkRed =>     "#8b0000",
201
			ConsoleColor.DarkGreen =>   "#006400",
202
			ConsoleColor.DarkYellow =>  "#8b8000",
203
			ConsoleColor.DarkCyan =>    "#008b8b",
204
			ConsoleColor.DarkMagenta => "#8b008b",
205
			ConsoleColor.DarkGray =>    "#a9a9a9",
206
			_ => throw new NotImplementedException(),
207
		};
208
	}
209

210
	public void ResetColor()
211
	{
212
		BackgroundColor = ConsoleColor.Black;
213
		ForegroundColor = ConsoleColor.White;
214
	}
215

216
	public BlazorConsole()
217
	{
218
		ActiveConsole = this;
219
		View = new Pixel[WindowHeight, WindowWidth];
220
		ClearNoRefresh();
221
	}
222

223
	public void DieIfNotActiveGame()
224
	{
225
		if (this != ActiveConsole)
226
		{
227
			throw new Exception("die :P pew pew");
228
		}
229
	}
230

231
	public async Task RefreshAndDelay(TimeSpan timeSpan)
232
	{
233
		DieIfNotActiveGame();
234
		if (StateHasChanged)
235
		{
236
			TriggerRefresh?.Invoke();
237
		}
238
		await Task.Delay(timeSpan);
239
	}
240

241
	public void HandleResize()
242
	{
243
		if (View.GetLength(0) != WindowHeight || View.GetLength(1) != WindowWidth)
244
		{
245
			Pixel[,] old_view = View;
246
			View = new Pixel[WindowHeight, WindowWidth];
247
			for (int row = 0; row < View.GetLength(0) && row < old_view.GetLength(0); row++)
248
			{
249
				for (int column = 0; column < View.GetLength(1) && column < old_view.GetLength(1); column++)
250
				{
251
					View[row, column] = old_view[row, column];
252
				}
253
			}
254
			StateHasChanged = true;
255
		}
256
	}
257

258
	public async Task Refresh()
259
	{
260
		DieIfNotActiveGame();
261
		if (StateHasChanged)
262
		{
263
			TriggerRefresh?.Invoke();
264
		}
265
		await Task.Delay(Delay);
266
	}
267

268
	public MarkupString State
269
	{
270
		get
271
		{
272
			StringBuilder stateBuilder = new();
273
			for (int row = 0; row < View.GetLength(0); row++)
274
			{
275
				for (int column = 0; column < View.GetLength(1); column++)
276
				{
277
					if (CursorVisible && (CursorLeft, CursorTop) == (column, row))
278
					{
279
						bool 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);
282
						stateBuilder.Append($@"<span class=""cursor {(isDark ? "cursor-dark" : "cursor-light")}"">");
283
					}
284
					if (View[row, column].BackgroundColor is not ConsoleColor.Black)
285
					{
286
						stateBuilder.Append($@"<span style=""background-color:{HtmlEncode(View[row, column].BackgroundColor)}"">");
287
					}
288
					if (View[row, column].ForegroundColor is not ConsoleColor.White)
289
					{
290
						stateBuilder.Append($@"<span style=""color:{HtmlEncode(View[row, column].ForegroundColor)}"">");
291
					}
292
					stateBuilder.Append(HttpUtility.HtmlEncode(View[row, column].Char));
293
					if (View[row, column].ForegroundColor is not ConsoleColor.White)
294
					{
295
						stateBuilder.Append("</span>");
296
					}
297
					if (View[row, column].BackgroundColor is not ConsoleColor.Black)
298
					{
299
						stateBuilder.Append("</span>");
300
					}
301
					if (CursorVisible && (CursorLeft, CursorTop) == (column, row))
302
					{
303
						stateBuilder.Append("</span>");
304
					}
305
				}
306
				stateBuilder.Append("<br />");
307
			}
308
			string state = stateBuilder.ToString();
309
			StateHasChanged = false;
310
			return (MarkupString)state;
311
		}
312
	}
313

314
	public void ResetColors()
315
	{
316
		DieIfNotActiveGame();
317
		BackgroundColor = ConsoleColor.Black;
318
		ForegroundColor = ConsoleColor.White;
319
	}
320

321
	public async Task Clear()
322
	{
323
		DieIfNotActiveGame();
324
		ClearNoRefresh();
325
		if (!RefreshOnInputOnly)
326
		{
327
			await Refresh();
328
		}
329
	}
330

331
	public void ClearNoRefresh()
332
	{
333
		DieIfNotActiveGame();
334
		for (int row = 0; row < View.GetLength(0); row++)
335
		{
336
			for (int column = 0; column < View.GetLength(1); column++)
337
			{
338
				Pixel pixel = new()
339
				{
340
					Char = ' ',
341
					BackgroundColor = BackgroundColor,
342
					ForegroundColor = ForegroundColor,
343
				};
344
				StateHasChanged = StateHasChanged || pixel != View[row, column];
345
				View[row, column] = pixel;
346
			}
347
		}
348
		(CursorLeft, CursorTop) = (0, 0);
349
	}
350

351
	public void WriteNoRefresh(char c)
352
	{
353
		DieIfNotActiveGame();
354
		if (c is '\r')
355
		{
356
			return;
357
		}
358
		if (c is '\n')
359
		{
360
			WriteLineNoRefresh();
361
			return;
362
		}
363
		if (CursorLeft >= View.GetLength(1))
364
		{
365
			(CursorLeft, CursorTop) = (0, CursorTop + 1);
366
		}
367
		if (CursorTop >= View.GetLength(0))
368
		{
369
			for (int row = 0; row < View.GetLength(0) - 1; row++)
370
			{
371
				for (int column = 0; column < View.GetLength(1); column++)
372
				{
373
					StateHasChanged = StateHasChanged || View[row, column] != View[row + 1, column];
374
					View[row, column] = View[row + 1, column];
375
				}
376
			}
377
			for (int column = 0; column < View.GetLength(1); column++)
378
			{
379
				Pixel pixel = new()
380
				{
381
					Char = ' ',
382
					BackgroundColor = BackgroundColor,
383
					ForegroundColor = ForegroundColor
384
				};
385
				StateHasChanged = StateHasChanged || View[View.GetLength(0) - 1, column] != pixel;
386
				View[View.GetLength(0) - 1, column] = pixel;
387
			}
388
			CursorTop--;
389
		}
390
		{
391
			Pixel pixel = new()
392
			{
393
				Char = c,
394
				BackgroundColor = BackgroundColor,
395
				ForegroundColor = ForegroundColor
396
			};
397
			StateHasChanged = StateHasChanged || View[CursorTop, CursorLeft] != pixel;
398
			View[CursorTop, CursorLeft] = pixel;
399
		}
400
		CursorLeft++;
401
	}
402

403
	public void WriteLineNoRefresh()
404
	{
405
		DieIfNotActiveGame();
406
		while (CursorLeft < View.GetLength(1))
407
		{
408
			WriteNoRefresh(' ');
409
		}
410
		(CursorLeft, CursorTop) = (0, CursorTop + 1);
411
	}
412

413
	public async Task Write(object o)
414
	{
415
		DieIfNotActiveGame();
416
		if (o is null) return;
417
		string? s = o.ToString();
418
		if (s is null || s is "") return;
419
		foreach (char c in s)
420
		{
421
			WriteNoRefresh(c);
422
		}
423
		if (!RefreshOnInputOnly)
424
		{
425
			await Refresh();
426
		}
427
	}
428

429
	public async Task WriteLine()
430
	{
431
		WriteLineNoRefresh();
432
		await Refresh();
433
	}
434

435
	public async Task WriteLine(object o)
436
	{
437
		if (o is not null)
438
		{
439
			string? s = o.ToString();
440
			if (s is not null)
441
			{
442
				foreach (char c in s)
443
				{
444
					WriteNoRefresh(c);
445
				}
446
			}
447
		}
448
		WriteLineNoRefresh();
449
		if (!RefreshOnInputOnly)
450
		{
451
			await Refresh();
452
		}
453
	}
454

455
	public ConsoleKeyInfo ReadKeyNoRefresh(bool capture)
456
	{
457
		if (!KeyAvailableNoRefresh())
458
		{
459
			throw new InvalidOperationException("attempting a no refresh ReadKey with an empty input buffer");
460
		}
461
		var keyInfo = InputBuffer.Dequeue();
462
		if (capture is false)
463
		{
464
			switch (keyInfo.KeyChar)
465
			{
466
				case '\n': WriteLineNoRefresh(); break;
467
				case '\0': break;
468
				case '\b': throw new NotImplementedException("ReadKey backspace not implemented");
469
				default: WriteNoRefresh(keyInfo.KeyChar); break;
470
			}
471
		}
472
		return keyInfo;
473
	}
474

475
	public async Task<ConsoleKeyInfo> ReadKey(bool capture)
476
	{
477
		while (!KeyAvailableNoRefresh())
478
		{
479
			await Refresh();
480
		}
481
		return ReadKeyNoRefresh(capture);
482
	}
483

484
	public async Task<string> ReadLine(bool v)
485
	{
486
		string line = string.Empty;
487
		while (true)
488
		{
489
			while (!KeyAvailableNoRefresh())
490
			{
491
				await Refresh();
492
			}
493
			var keyInfo = InputBuffer.Dequeue();
494
			switch (keyInfo.Key)
495
			{
496
				case ConsoleKey.Backspace:
497
					if (line.Length > 0)
498
					{
499
						if (CursorLeft > 0)
500
						{
501
							CursorLeft--;
502
							StateHasChanged = true;
503
							View[CursorTop, CursorLeft].Char = ' ';
504
						}
505
						line = line[..^1];
506
						await Refresh();
507
					}
508
					break;
509
				case ConsoleKey.Enter:
510
					WriteLineNoRefresh();
511
					await Refresh();
512
					return line;
513
				default:
514
					if (keyInfo.KeyChar is not '\0')
515
					{
516
						line += keyInfo.KeyChar;
517
						WriteNoRefresh(keyInfo.KeyChar);
518
						await Refresh();
519
					}
520
					break;
521
			}
522
		}
523
	}
524

525
	public bool KeyAvailableNoRefresh()
526
	{
527
		return InputBuffer.Count > 0;
528
	}
529

530
	public async Task<bool> KeyAvailable()
531
	{
532
		await Refresh();
533
		return KeyAvailableNoRefresh();
534
	}
535

536
	public async Task SetCursorPosition(int left, int top)
537
	{
538
		(CursorLeft, CursorTop) = (left, top);
539
		if (!RefreshOnInputOnly)
540
		{
541
			await Refresh();
542
		}
543
	}
544

545
	public async Task PromptPressToContinue(string? prompt = null, ConsoleKey key = ConsoleKey.Enter)
546
	{
547
		if (!key.IsDefined())
548
		{
549
			throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(ConsoleKey)} enum");
550
		}
551
		prompt ??= $"Press [{key}] to continue...";
552
		foreach (char c in prompt)
553
		{
554
			WriteNoRefresh(c);
555
		}
556
		await PressToContinue(key);
557
	}
558

559
	public async Task PressToContinue(ConsoleKey key = ConsoleKey.Enter)
560
	{
561
		if (!key.IsDefined())
562
		{
563
			throw new ArgumentOutOfRangeException(nameof(key), key, $"{nameof(key)} is not a defined value in the {nameof(ConsoleKey)} enum");
564
		}
565
		while ((await ReadKey(true)).Key != key)
566
		{
567
			continue;
568
		}
569
	}
570

571
#pragma warning disable CA1822 // Mark members as static
572
	/// <summary>
573
	/// Returns true. Some members of <see cref="Console"/> only work
574
	/// on Windows such as <see cref="Console.WindowWidth"/>, but even though this
575
	/// is blazor and not necessarily on Windows, this wrapper contains implementations
576
	/// for those Windows-only members.
577
	/// </summary>
578
	/// <returns>true</returns>
579
	public bool IsWindows() => true;
580
#pragma warning restore CA1822 // Mark members as static
581
}

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.