Compare commits
4 Commits
0d5961566b
...
07672d3463
Author | SHA1 | Date |
---|---|---|
Gottfried Wilhelm Leibniz | 07672d3463 | |
Gottfried Wilhelm Leibniz | 64d18102be | |
Gottfried Wilhelm Leibniz | 626b31a3ce | |
Gottfried Wilhelm Leibniz | f51bd2bb44 |
|
@ -0,0 +1,9 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace GottfriedsNackenUtility;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bietet nütliche mathematische Funktionen.
|
||||||
|
/// </summary>
|
||||||
|
public static class MathHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enumeriert die Zahlen der Fibonacci-Folge.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Der Datentyp ist <see cref="BigInteger"/>, da es ein paar Fibonacci-Zahlen gibt die nicht von <see cref="int"/> dargestellt werden können.
|
||||||
|
/// </remarks>
|
||||||
|
public static IEnumerable<BigInteger> Fibonacci()
|
||||||
|
{
|
||||||
|
BigInteger i = 0;
|
||||||
|
BigInteger j = 1;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
BigInteger fib = i + j;
|
||||||
|
|
||||||
|
i = j;
|
||||||
|
j = fib;
|
||||||
|
|
||||||
|
yield return fib;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace GottfriedsNackenUtility;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hier sind suspiziöse Funktionen versteckt.
|
||||||
|
/// </summary>
|
||||||
|
public static class SusHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dies ist der ausgeklügelste fake-Fortschrittbalkensalgorithmus den die Welt je erblickt hat.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Dieser Task läuft in einer Endlosschleife und MUSS mit Hilfe des Abbruchtokens abgebrochen werden!
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="progressChangedCallback">Das callback das aufgerufen wird, wenn der Fortschritt sich geändert hat.
|
||||||
|
/// Gibt einen Int zwischen 0 und 100 zurück oder -1, wenn der Fortschrittsbalken auf einen "indefinite" Zustand wechseln soll.</param>
|
||||||
|
/// <param name="cancellationToken">Ein Abbruchtoken, das beim Warten auf den Abschluss der Aufgabe überwacht werden soll.</param>
|
||||||
|
public static async Task UpdateDefinitelyNotFakeProgress(Action<int> progressChangedCallback, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (BigInteger fibonacci in MathHelper.Fibonacci())
|
||||||
|
{
|
||||||
|
await Task.Delay(250 + (int)(fibonacci % 420), cancellationToken);
|
||||||
|
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (fibonacci.IsEven && (int)(fibonacci % 42) == 0)
|
||||||
|
{
|
||||||
|
progressChangedCallback(-1);
|
||||||
|
await Task.Delay(1337, cancellationToken);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChangedCallback((int)(fibonacci % 69));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException) { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ VisualStudioVersion = 17.1.32210.238
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GottfriedsNackenWebseite", "GottfriedsNackenWebseite\GottfriedsNackenWebseite.csproj", "{464DD643-DAD9-4E16-BB24-36F789ED4C15}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GottfriedsNackenWebseite", "GottfriedsNackenWebseite\GottfriedsNackenWebseite.csproj", "{464DD643-DAD9-4E16-BB24-36F789ED4C15}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GottfriedsNackenUtility", "GottfriedsNackenUtility\GottfriedsNackenUtility.csproj", "{0122762F-9961-4D28-8AA9-0D6AE7630208}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -15,6 +17,10 @@ Global
|
||||||
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Release|Any CPU.Build.0 = Release|Any CPU
|
{464DD643-DAD9-4E16-BB24-36F789ED4C15}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0122762F-9961-4D28-8AA9-0D6AE7630208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0122762F-9961-4D28-8AA9-0D6AE7630208}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0122762F-9961-4D28-8AA9-0D6AE7630208}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0122762F-9961-4D28-8AA9-0D6AE7630208}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -0,0 +1,235 @@
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<!-- Hidden Alerts -->
|
||||||
|
<div hidden="@(_alert == AlertState.None)">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Small">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="1"></MudItem>
|
||||||
|
<MudItem xs="8">
|
||||||
|
@if (_alert == AlertState.Correct)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Success">Korrekt!</MudAlert>
|
||||||
|
}
|
||||||
|
else if (_alert == AlertState.Invalid)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Error">Eingabe ungültig!</MudAlert>
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="2">
|
||||||
|
@if (_alert == AlertState.Correct)
|
||||||
|
{
|
||||||
|
<MudIconButton Icon="@Icons.Filled.Replay" Color="Color.Primary" aria-label="nochmal spielen" OnClick="@PlayAgain" />
|
||||||
|
}
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="1"></MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Matrix mit Buchstabenfeldern -->
|
||||||
|
<div class="d-flex justify-center mt-2 mb-2">
|
||||||
|
<div>
|
||||||
|
@foreach (CharData[] guess in _previousGuesses)
|
||||||
|
{
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
@foreach (CharData cd in guess)
|
||||||
|
{
|
||||||
|
if (cd.State == CharState.Correct)
|
||||||
|
{
|
||||||
|
<MudPaper Style="border-color: green; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
|
<MudText Typo="Typo.h3" Align="Align.Center">@cd.Char</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
else if (cd.State == CharState.Misplaced)
|
||||||
|
{
|
||||||
|
<MudPaper Style="border-color: yellow; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
|
<MudText Typo="Typo.h3" Align="Align.Center">@cd.Char</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudPaper Style="border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
|
<MudText Typo="Typo.h3" Align="Align.Center">@cd.Char</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@*
|
||||||
|
<MudTextField T="string" FullWidth="true" Immediate="true" @bind-Value="Input" Label="Bla" Variant="Variant.Outlined" MaxLength="ColumnCount" Counter="ColumnCount" />
|
||||||
|
<MudTextField T="string" FullWidth="true" Immediate="true" @bind-Value="Input" Label="Blub" Variant="Variant.Outlined" MaxLength="ColumnCount" Counter="ColumnCount" />
|
||||||
|
*@
|
||||||
|
<!-- Input dialogue feld -->
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Small">
|
||||||
|
<MudGrid Spacing="2">
|
||||||
|
<MudItem xs="1" />
|
||||||
|
<MudItem xs="8">
|
||||||
|
<MudTextField @ref="_textField" T="string" FullWidth="true" Immediate="true" @bind-Value="Input" Label="Dein Tipp:" Variant="Variant.Outlined" MaxLength="ColumnCount" Mask="@InputMask" Counter="ColumnCount" OnKeyDown="OnKeyDown" />
|
||||||
|
@*<input Type="text" value="@Input"/>*@
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="2">
|
||||||
|
<MudButton style="height: 56px; margin-top: 6px;" Class="justify-center" Disabled="Input.Length != ColumnCount"
|
||||||
|
Size="Size.Large" Variant="Variant.Outlined" EndIcon="@Icons.Material.Filled.Send" Color="Color.Primary" OnClick=@OnEnterGuessClick>OK</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="1" />
|
||||||
|
</MudGrid>
|
||||||
|
</MudContainer>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private enum AlertState
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Correct,
|
||||||
|
Invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum CharState
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Correct,
|
||||||
|
Misplaced,
|
||||||
|
Wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct CharData
|
||||||
|
{
|
||||||
|
public char Char;
|
||||||
|
public CharState State;
|
||||||
|
|
||||||
|
public CharData(char c, CharState state)
|
||||||
|
{
|
||||||
|
Char = c;
|
||||||
|
State = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _found = false;
|
||||||
|
|
||||||
|
private AlertState _alert = AlertState.None;
|
||||||
|
|
||||||
|
private List<CharData[]> _previousGuesses = new();
|
||||||
|
|
||||||
|
private MudTextField<string> _textField = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Input { get; set; } = String.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int ColumnCount { get; set; }
|
||||||
|
|
||||||
|
public string? Secret { get; set; } = null;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IMask InputMask { get; set; } = new RegexMask("^.*$");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prüft ob die getätigte Eingabe eine mögliche Lösung ist.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Func<string, bool> CheckInput { get; set; } = _ => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wird ausgelöst wenn eine Eingabe getätigt und validiert wurde.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<CharData[]> InputValidated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wird ausgelöst wenn ein neues Secret generiert werden muss.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback GenerateSecret { get; set; }
|
||||||
|
|
||||||
|
public async Task OnKeyDown(KeyboardEventArgs e)
|
||||||
|
{
|
||||||
|
_alert = AlertState.None;
|
||||||
|
|
||||||
|
if (e.Code == "Enter" || e.Code == "NumpadEnter")
|
||||||
|
{
|
||||||
|
if(_found)
|
||||||
|
{
|
||||||
|
await PlayAgain();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await OnEnterGuessClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task OnEnterGuessClick()
|
||||||
|
{
|
||||||
|
if (Input.Length != ColumnCount || _found) return;
|
||||||
|
|
||||||
|
if (!CheckInput(Input))
|
||||||
|
{
|
||||||
|
_alert = AlertState.Invalid;
|
||||||
|
StateHasChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Secret == null)
|
||||||
|
await GenerateSecret.InvokeAsync();
|
||||||
|
|
||||||
|
if(await TestInput(Input))
|
||||||
|
{
|
||||||
|
_found = true;
|
||||||
|
_alert = AlertState.Correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _textField.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prüft ob die Eingabe korrekt ist.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<bool> TestInput(string input)
|
||||||
|
{
|
||||||
|
CharData[] validatedInput = new CharData[ColumnCount];
|
||||||
|
|
||||||
|
bool correct = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < ColumnCount; i++)
|
||||||
|
{
|
||||||
|
char guessedChar = char.ToUpper(input[i]);
|
||||||
|
|
||||||
|
CharState state;
|
||||||
|
|
||||||
|
if (guessedChar == char.ToUpper(Secret![i]))
|
||||||
|
{
|
||||||
|
state = CharState.Correct;
|
||||||
|
}
|
||||||
|
else if (Secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
state = CharState.Misplaced;
|
||||||
|
correct = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state = CharState.Wrong;
|
||||||
|
correct = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
validatedInput[i] = new CharData(guessedChar, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
_previousGuesses.Add(validatedInput);
|
||||||
|
|
||||||
|
await InputValidated.InvokeAsync(validatedInput);
|
||||||
|
|
||||||
|
return correct;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlayAgain()
|
||||||
|
{
|
||||||
|
_previousGuesses.Clear();
|
||||||
|
_alert = AlertState.None;
|
||||||
|
_found = false;
|
||||||
|
|
||||||
|
await GenerateSecret.InvokeAsync();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
<!-- Matrix mit Buchstabenfeldern -->
|
||||||
|
<div class="d-flex justify-center mt-2 mb-2">
|
||||||
|
<div>
|
||||||
|
@{
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
for (int row = 0; row < Math.Ceiling((double)Keys.Length / KeysPerRow); row++)
|
||||||
|
{
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
@for (int i = 0; i < KeysPerRow && index < Keys.Length; i++, index++)
|
||||||
|
{
|
||||||
|
char key = Keys[index];
|
||||||
|
|
||||||
|
char upperKey = char.ToUpper(key);
|
||||||
|
|
||||||
|
_knownKeyStates.TryGetValue(upperKey, out var keyState);
|
||||||
|
|
||||||
|
if (keyState == WordleComponent.CharState.Correct)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined" Class="mr-2" Style="width: 5em; height: 5em" Color="Color.Success">
|
||||||
|
<MudText Typo="Typo.h5">@upperKey</MudText>
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else if (keyState == WordleComponent.CharState.Misplaced)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined" Class="mr-2" Style="width: 5em; height: 5em" Color="Color.Warning">
|
||||||
|
<MudText Typo="Typo.h5">@upperKey</MudText>
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else if (keyState == WordleComponent.CharState.Wrong)
|
||||||
|
{
|
||||||
|
<MudButton Disabled="true" Variant="Variant.Outlined" Class="mr-2" Style="width: 5em; height: 5em" Color="Color.Error">
|
||||||
|
<MudText Typo="Typo.h5">@upperKey</MudText>
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Outlined" Class="mr-2" Style="width: 5em; height: 5em">
|
||||||
|
<MudText Typo="Typo.h5">@upperKey</MudText>
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public char[] Keys { get; set; } = Array.Empty<char>();
|
||||||
|
|
||||||
|
private Dictionary<char, WordleComponent.CharState> _knownKeyStates = new();
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public int KeysPerRow { get; set; } = 10;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Secret { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public EventCallback<char> KeyPressed { get; set; }
|
||||||
|
|
||||||
|
private async Task ButtonPressed(char c)
|
||||||
|
{
|
||||||
|
await KeyPressed.InvokeAsync(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateKeyInfo(WordleComponent.CharData cd)
|
||||||
|
{
|
||||||
|
_knownKeyStates.TryGetValue(cd.Char, out var oldState);
|
||||||
|
|
||||||
|
WordleComponent.CharState newState = WordleComponent.CharState.None;
|
||||||
|
|
||||||
|
switch (cd.State)
|
||||||
|
{
|
||||||
|
case WordleComponent.CharState.Correct:
|
||||||
|
newState = WordleComponent.CharState.Correct;
|
||||||
|
break;
|
||||||
|
case WordleComponent.CharState.Misplaced:
|
||||||
|
// Den besseren Zustand nicht durch schlechteren ersetzen
|
||||||
|
if (oldState != WordleComponent.CharState.Correct)
|
||||||
|
{
|
||||||
|
newState = WordleComponent.CharState.Misplaced;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WordleComponent.CharState.Wrong:
|
||||||
|
newState = WordleComponent.CharState.Wrong;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
_knownKeyStates[cd.Char] = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetKeyInfo()
|
||||||
|
{
|
||||||
|
_knownKeyStates.Clear();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,4 +17,8 @@
|
||||||
<PackageReference Include="MudBlazor" Version="6.0.7" />
|
<PackageReference Include="MudBlazor" Version="6.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GottfriedsNackenUtility\GottfriedsNackenUtility.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
namespace GottfriedsNackenWebseite.Numberle;
|
||||||
|
|
||||||
|
public abstract class AstNode
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AstBinaryExpression : AstNode
|
||||||
|
{
|
||||||
|
public AstNode Left;
|
||||||
|
public AstNode Right;
|
||||||
|
|
||||||
|
public string Operator;
|
||||||
|
|
||||||
|
public AstBinaryExpression(string @operator, AstNode left, AstNode right)
|
||||||
|
{
|
||||||
|
Operator = @operator;
|
||||||
|
Left = left;
|
||||||
|
Right = right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AstNumericLiteral : AstNode
|
||||||
|
{
|
||||||
|
public int Value;
|
||||||
|
|
||||||
|
public AstNumericLiteral(int value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
namespace GottfriedsNackenWebseite.Numberle;
|
||||||
|
|
||||||
|
public class Parser
|
||||||
|
{
|
||||||
|
private string _input = string.Empty;
|
||||||
|
|
||||||
|
private readonly Tokenizer _tokenizer = new();
|
||||||
|
|
||||||
|
private Token _lookahead;
|
||||||
|
|
||||||
|
private TokenType NextTokenType => _lookahead.Type;
|
||||||
|
|
||||||
|
public AstNode Parse(string input)
|
||||||
|
{
|
||||||
|
_input = input;
|
||||||
|
_tokenizer.Init(_input);
|
||||||
|
|
||||||
|
// Prime the tokenizer to obtain the first token
|
||||||
|
// which is our lookahead. The lookahead is used
|
||||||
|
// for predictive parsing.
|
||||||
|
_lookahead = _tokenizer.GetNextToken();
|
||||||
|
|
||||||
|
// Parse recursively starting from the main entry point, the Program:
|
||||||
|
|
||||||
|
return ParseEqualityExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
private AstNode ParseBinaryExpression(Func<AstNode> builder, TokenType operatorType)
|
||||||
|
{
|
||||||
|
AstNode left = builder();
|
||||||
|
|
||||||
|
while (_lookahead.Type == operatorType)
|
||||||
|
{
|
||||||
|
string @operator = (string)Consume(operatorType).Value!;
|
||||||
|
|
||||||
|
AstNode right = builder();
|
||||||
|
|
||||||
|
left = new AstBinaryExpression(@operator, left, right);
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EqualityExpression
|
||||||
|
/// : AdditiveExpression
|
||||||
|
/// | AdditiveExpression '=' EqualityExpression
|
||||||
|
/// ;
|
||||||
|
/// </summary>
|
||||||
|
private AstNode ParseEqualityExpression()
|
||||||
|
{
|
||||||
|
return ParseBinaryExpression(ParseAdditiveExpression, TokenType.Equals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AdditiveExpression:
|
||||||
|
/// : MultiplicativeExpression
|
||||||
|
/// | MultiplicativeExpression ADDITIVE_OPERATOR AdditiveExpression
|
||||||
|
/// ;
|
||||||
|
/// </summary>
|
||||||
|
private AstNode ParseAdditiveExpression()
|
||||||
|
{
|
||||||
|
return ParseBinaryExpression(ParseMultiplicativeExpression, TokenType.AdditiveOperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MultiplicativeExpression:
|
||||||
|
/// : NumericLiteral
|
||||||
|
/// | MultiplicativeExpression MULTIPLICATIVE_OPERATOR NumericLiteral
|
||||||
|
/// ;
|
||||||
|
/// </summary>
|
||||||
|
private AstNode ParseMultiplicativeExpression()
|
||||||
|
{
|
||||||
|
return ParseBinaryExpression(ParseNumericLiteral, TokenType.MultiplicativeOperator);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AstNumericLiteral
|
||||||
|
/// : NumberLiteral
|
||||||
|
/// ;
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private AstNumericLiteral ParseNumericLiteral()
|
||||||
|
{
|
||||||
|
Token token = Consume(TokenType.NumberLiteral);
|
||||||
|
|
||||||
|
return new AstNumericLiteral((int)token.Value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Token Consume(TokenType type)
|
||||||
|
{
|
||||||
|
Token token = _lookahead;
|
||||||
|
|
||||||
|
if (token.Type == TokenType.EndOfFile)
|
||||||
|
throw new Exception($"Unexpected end of input, expected \"{type}\"");
|
||||||
|
|
||||||
|
if (token.Type != type)
|
||||||
|
throw new Exception($"Unexpected token \"{token.Type}\", expected \"{type}\"");
|
||||||
|
|
||||||
|
_lookahead = _tokenizer.GetNextToken();
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
namespace GottfriedsNackenWebseite.Numberle;
|
||||||
|
|
||||||
|
public enum TokenType
|
||||||
|
{
|
||||||
|
Unknown,
|
||||||
|
EndOfFile,
|
||||||
|
|
||||||
|
AdditiveOperator,
|
||||||
|
MultiplicativeOperator,
|
||||||
|
Equals,
|
||||||
|
|
||||||
|
NumberLiteral
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Token
|
||||||
|
{
|
||||||
|
public TokenType Type;
|
||||||
|
public object? Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Tokenizer
|
||||||
|
{
|
||||||
|
private string _input = string.Empty;
|
||||||
|
private int _cursor = 0;
|
||||||
|
|
||||||
|
public void Init(string input)
|
||||||
|
{
|
||||||
|
_input = input;
|
||||||
|
_cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasMoreTokens => _cursor < _input.Length;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt das aktuelle Zeichen der Eingabe zurück oder das Null-Zeichen ('\0') wenn das Ende der Eingabe erreicht wurde.
|
||||||
|
/// </summary>
|
||||||
|
private char CurrentChar => HasMoreTokens ? _input[_cursor] : '\0';
|
||||||
|
|
||||||
|
public Token GetNextToken()
|
||||||
|
{
|
||||||
|
if (!HasMoreTokens)
|
||||||
|
{
|
||||||
|
return new Token
|
||||||
|
{
|
||||||
|
Type = TokenType.EndOfFile
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Token token = new Token
|
||||||
|
{
|
||||||
|
Type = TokenType.Unknown,
|
||||||
|
Value = null
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (CurrentChar)
|
||||||
|
{
|
||||||
|
case '+':
|
||||||
|
token.Type = TokenType.AdditiveOperator;
|
||||||
|
token.Value = "+";
|
||||||
|
_cursor++;
|
||||||
|
break;
|
||||||
|
case '-':
|
||||||
|
token.Type = TokenType.AdditiveOperator;
|
||||||
|
token.Value = "-";
|
||||||
|
_cursor++;
|
||||||
|
break;
|
||||||
|
case '*':
|
||||||
|
token.Type = TokenType.MultiplicativeOperator;
|
||||||
|
token.Value = "*";
|
||||||
|
_cursor++;
|
||||||
|
break;
|
||||||
|
case '/':
|
||||||
|
token.Type = TokenType.MultiplicativeOperator;
|
||||||
|
token.Value = "/";
|
||||||
|
_cursor++;
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
token.Type = TokenType.Equals;
|
||||||
|
token.Value = "=";
|
||||||
|
_cursor++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (char.IsDigit(CurrentChar))
|
||||||
|
{
|
||||||
|
token.Type = TokenType.NumberLiteral;
|
||||||
|
|
||||||
|
int value = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (!HasMoreTokens)
|
||||||
|
break;
|
||||||
|
|
||||||
|
char c = CurrentChar;
|
||||||
|
|
||||||
|
if (char.IsDigit(c))
|
||||||
|
{
|
||||||
|
value *= 10;
|
||||||
|
value += c - '0';
|
||||||
|
|
||||||
|
_cursor++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
token.Value = value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
@page "/numberle"
|
||||||
|
|
||||||
|
@using GottfriedsNackenWebseite.Components
|
||||||
|
@using GottfriedsNackenWebseite.Numberle
|
||||||
|
@using GottfriedsNackenUtility
|
||||||
|
@using System.Diagnostics
|
||||||
|
@using System.Numerics
|
||||||
|
|
||||||
|
<PageTitle>Nacken Numberle</PageTitle>
|
||||||
|
<MudText Align="Align.Center" Class="mt-2 mb-2"> Gebe eine (korrekte) mathematische Gleichung ein:</MudText>
|
||||||
|
|
||||||
|
<WordleComponent ColumnCount="8" GenerateSecret="OnGenerateNewSecret" InputMask="@_inputMask" CheckInput="CheckInput" InputValidated="OnInputValidated" @ref="_wordle" />
|
||||||
|
|
||||||
|
<WordleKeyboardComponent @ref="_keyboard" KeyPressed="KeyboardKeyPressed" Keys="Chars" />
|
||||||
|
|
||||||
|
<MudOverlay @bind-Visible="_showLoadingOverlay" DarkBackground="true">
|
||||||
|
<MudCard>
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudText Typo="Typo.h5">Bitte warten...</MudText>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudText>Erzeuge eine unknackbare Formel mit roher Gewalt!</MudText>
|
||||||
|
<MudProgressLinear Color="Color.Secondary" Indeterminate="_progress < 0" Value="_progress" Class="my-7"/>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
</MudOverlay>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private static readonly IMask _inputMask = new RegexMask(@"^[0-9+\-*/=]{0,8}$");
|
||||||
|
|
||||||
|
private readonly Parser _parser = new();
|
||||||
|
|
||||||
|
private WordleComponent _wordle = default!;
|
||||||
|
private WordleKeyboardComponent _keyboard = default!;
|
||||||
|
|
||||||
|
private bool _showLoadingOverlay;
|
||||||
|
private int _progress;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await base.OnInitializedAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prüft ob input eine gültige Gleichung ist.
|
||||||
|
/// </summary>
|
||||||
|
private bool CheckInput(string input)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AstNode ast = _parser.Parse(input);
|
||||||
|
|
||||||
|
// Äußerste Node muss '=' sein.
|
||||||
|
if (ast is not AstBinaryExpression binExp || binExp.Operator != "=")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
(decimal value, int equalSigns) = CrunchyValidaty(ast);
|
||||||
|
|
||||||
|
// Wir akzeptieren nur, wenn es exakt ein Gleichheitszeichen gibt und die Gleichung erfüllt ist.
|
||||||
|
return value == 1 && equalSigns == 1;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Berechnet den Wert der gegebenen (Teil-)Gleichung und zählt wie viele Gleichheitszeichen darin vorkommen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="node">Die zu berechnende Gleichung.</param>
|
||||||
|
/// <returns>Den Wert der Gleichung und die Anzahl Gleichheitszeichen</returns>
|
||||||
|
private (decimal Value, int equalSigns) CrunchyValidaty(AstNode node)
|
||||||
|
{
|
||||||
|
switch (node)
|
||||||
|
{
|
||||||
|
case AstNumericLiteral number:
|
||||||
|
return (number.Value, 0);
|
||||||
|
case AstBinaryExpression binary:
|
||||||
|
|
||||||
|
(decimal Value, int equalSigns) left = CrunchyValidaty(binary.Left);
|
||||||
|
(decimal Value, int equalSigns) right = CrunchyValidaty(binary.Right);
|
||||||
|
|
||||||
|
int equalsSigns = left.equalSigns + right.equalSigns;
|
||||||
|
|
||||||
|
switch (binary.Operator)
|
||||||
|
{
|
||||||
|
case "+":
|
||||||
|
return (left.Value + right.Value, equalsSigns);
|
||||||
|
case "-":
|
||||||
|
return (left.Value - right.Value, equalsSigns);
|
||||||
|
case "*":
|
||||||
|
return (left.Value * right.Value, equalsSigns);
|
||||||
|
case "/":
|
||||||
|
return (left.Value / right.Value, equalsSigns);
|
||||||
|
case "=":
|
||||||
|
return (left.Value == right.Value ? 1 : 0, equalsSigns + 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Die Zeichen aus denen eine Antwort besteht.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly char[] Chars = {
|
||||||
|
'=', '+', '-', '*', '/',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generiert ein neues Geheimnis und zeigt während dessen den Ladebalken an.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
private async Task OnGenerateNewSecret()
|
||||||
|
{
|
||||||
|
_keyboard.ResetKeyInfo();
|
||||||
|
|
||||||
|
CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Task? amazingTask = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ladebalkenaktualisierer starten
|
||||||
|
amazingTask = SusHelper.UpdateDefinitelyNotFakeProgress(progress =>
|
||||||
|
{
|
||||||
|
_progress = progress;
|
||||||
|
StateHasChanged();
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
_showLoadingOverlay = true;
|
||||||
|
|
||||||
|
_wordle.Secret = await Task.Run(GenerateFormula);
|
||||||
|
|
||||||
|
_showLoadingOverlay = false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Ladebalkenaktualisierer stoppen
|
||||||
|
cts.Cancel();
|
||||||
|
if (amazingTask != null)
|
||||||
|
await amazingTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erzeugt eine neue (gültige) Formel.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Die Formel</returns>
|
||||||
|
private string GenerateFormula()
|
||||||
|
{
|
||||||
|
int charCount = 8;
|
||||||
|
|
||||||
|
char[] formula = new char[charCount];
|
||||||
|
|
||||||
|
// Gleichzeichen plazieren
|
||||||
|
int equalsIndex = Random.Shared.Next(1, charCount - 1);
|
||||||
|
formula[equalsIndex] = '=';
|
||||||
|
|
||||||
|
// Ersetzt das Zeichen mit Index "index" durch einen Zufälligen wert.
|
||||||
|
// TODO: ein etwas gewiefterer Algorithmus würde nicht schaden.
|
||||||
|
void PlaceRandomChar(int index)
|
||||||
|
{
|
||||||
|
char left = (index > 0) ? formula[index - 1] : '\0';
|
||||||
|
char right = (index < charCount - 1) ? formula[index + 1] : '\0';
|
||||||
|
|
||||||
|
bool canPlaceOperator = char.IsDigit(left) && char.IsDigit(right);
|
||||||
|
|
||||||
|
if (canPlaceOperator)
|
||||||
|
formula[index] = Chars[Random.Shared.Next(1, Chars.Length)];
|
||||||
|
else
|
||||||
|
formula[index] = Chars[Random.Shared.Next(6, Chars.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ROHE GEWALT!
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < formula.Length; i++)
|
||||||
|
{
|
||||||
|
if (i == equalsIndex)
|
||||||
|
i++;
|
||||||
|
|
||||||
|
PlaceRandomChar(i);
|
||||||
|
|
||||||
|
string str = new string(formula);
|
||||||
|
|
||||||
|
Debug.WriteLine(str);
|
||||||
|
|
||||||
|
if (CheckInput(str))
|
||||||
|
{
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task KeyboardKeyPressed(char key)
|
||||||
|
{
|
||||||
|
// TODO: Virtuelles Keyboard
|
||||||
|
//await _wordle.SetText(_wordle.Input + key); //Input += key;
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInputValidated(WordleComponent.CharData[] data)
|
||||||
|
{
|
||||||
|
// Auswertung in das Keyboard packen
|
||||||
|
foreach (WordleComponent.CharData cd in data)
|
||||||
|
{
|
||||||
|
_keyboard.UpdateKeyInfo(cd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,26 +25,26 @@
|
||||||
@foreach(var guess in PreviousGuesses)
|
@foreach(var guess in PreviousGuesses)
|
||||||
{
|
{
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
|
|
||||||
<!-- WHY FAILURE?????????????????????????? -->
|
|
||||||
@for(int i = 0; i < 5; i++)
|
@for(int i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
<!-- Wer hat sich denn sowas ausgedacht? -->
|
char guessedChar = char.ToUpper(guess[i]);
|
||||||
var j = i;
|
|
||||||
if(@guess.ToUpper()[j] == secret.ToUpper()[j])
|
if(guessedChar == char.ToUpper(secret[i]))
|
||||||
{
|
{
|
||||||
<MudPaper Style="border-color: green; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
<MudPaper Style="border-color: green; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
<MudText Typo="Typo.h3" Align="Align.Center">@guess.ToUpper()[j]</MudText>
|
<MudText Typo="Typo.h3" Align="Align.Center">@guessedChar</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}else if (secret.ToUpper().Contains(@guess.ToUpper()[j]))
|
}
|
||||||
|
else if (secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase))
|
||||||
{
|
{
|
||||||
<MudPaper Style="border-color: yellow; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
<MudPaper Style="border-color: yellow; border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
<MudText Typo="Typo.h3" Align="Align.Center">@guess.ToUpper()[j]</MudText>
|
<MudText Typo="Typo.h3" Align="Align.Center">@guessedChar</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}else
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<MudPaper Style="border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
<MudPaper Style="border-width: 4px;" Class="mr-2" Width="60px" Height="60px" Outlined="true">
|
||||||
<MudText Typo="Typo.h3" Align="Align.Center">@guess.ToUpper()[j]</MudText>
|
<MudText Typo="Typo.h3" Align="Align.Center">@guessedChar</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
<MudGrid Spacing="2">
|
<MudGrid Spacing="2">
|
||||||
<MudItem xs="1"></MudItem>
|
<MudItem xs="1"></MudItem>
|
||||||
<MudItem xs="8">
|
<MudItem xs="8">
|
||||||
<MudTextField Class="" @ref="textField" T="string" FullWidth="true" @bind-Value="Input" Label="Dein Tipp:" Variant="Variant.Outlined" MaxLength="5" Mask="@mask">Hi</MudTextField>
|
<MudTextField Class="" @ref="textField" T="string" FullWidth="true" @bind-Value="Input" Label="Dein Tipp:" Variant="Variant.Outlined" MaxLength="5" Mask="@_inputMask">Hi</MudTextField>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="2">
|
<MudItem xs="2">
|
||||||
<MudButton style="height:56px; margin-top:6px;" Class="justify-center" Size="Size.Large" Variant="Variant.Outlined" EndIcon="@Icons.Material.Filled.Send" Color="Color.Primary" OnClick=@ButtonOnClick>OK</MudButton>
|
<MudButton style="height:56px; margin-top:6px;" Class="justify-center" Size="Size.Large" Variant="Variant.Outlined" EndIcon="@Icons.Material.Filled.Send" Color="Color.Primary" OnClick=@ButtonOnClick>OK</MudButton>
|
||||||
|
@ -78,7 +78,10 @@
|
||||||
public List<string> SecretList = new List<string>();
|
public List<string> SecretList = new List<string>();
|
||||||
|
|
||||||
//Keine Ahnung wie das funktionert... Danke StackOverflow :)
|
//Keine Ahnung wie das funktionert... Danke StackOverflow :)
|
||||||
public IMask mask = new RegexMask(@"^[A-Za-z]{0,5}$");
|
/// <summary>
|
||||||
|
/// Eine Regexmaske die alle Strings matched die 0 bis 5 Buchstaben enthalten.
|
||||||
|
/// </summary>
|
||||||
|
private IMask _inputMask = new RegexMask(@"^[A-Za-z]{0,5}$");
|
||||||
|
|
||||||
public string secret = "penis";
|
public string secret = "penis";
|
||||||
private bool found = false;
|
private bool found = false;
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
<MudNavLink Href="nackendex" Match=NavLinkMatch.Prefix>
|
<MudNavLink Href="nackendex" Match=NavLinkMatch.Prefix>
|
||||||
NackenDex
|
NackenDex
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="numberle" Match=NavLinkMatch.Prefix>
|
||||||
|
Nacken Numberle
|
||||||
|
</MudNavLink>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
@*
|
@*
|
||||||
<div class="top-row ps-3 navbar navbar-dark">
|
<div class="top-row ps-3 navbar navbar-dark">
|
||||||
|
|
Loading…
Reference in New Issue