Merge branch 'NackenNumberle' into main

This commit is contained in:
Gottfried Wilhelm Leibniz 2022-04-20 16:28:34 +02:00
commit 07672d3463
8 changed files with 440 additions and 69 deletions

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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;
}
}
}

View File

@ -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) { }
}
}

View File

@ -5,6 +5,8 @@ VisualStudioVersion = 17.1.32210.238
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GottfriedsNackenWebseite", "GottfriedsNackenWebseite\GottfriedsNackenWebseite.csproj", "{464DD643-DAD9-4E16-BB24-36F789ED4C15}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GottfriedsNackenUtility", "GottfriedsNackenUtility\GottfriedsNackenUtility.csproj", "{0122762F-9961-4D28-8AA9-0D6AE7630208}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -1,13 +1,25 @@
<!-- Hidden Alerts -->
<div hidden="@hideAlert">
@using System.Diagnostics
<!-- Hidden Alerts -->
<div hidden="@(_alert == AlertState.None)">
<MudContainer MaxWidth="MaxWidth.Small">
<MudGrid>
<MudItem xs="1"></MudItem>
<MudItem xs="8">
<MudAlert Severity="Severity.Success">Korrekt!</MudAlert>
@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">
<MudIconButton Icon="@Icons.Filled.Replay" Color="Color.Primary" aria-label="nochmal spielen" OnClick="@PlayAgain"></MudIconButton>
@if (_alert == AlertState.Correct)
{
<MudIconButton Icon="@Icons.Filled.Replay" Color="Color.Primary" aria-label="nochmal spielen" OnClick="@PlayAgain" />
}
</MudItem>
<MudItem xs="1"></MudItem>
</MudGrid>
@ -17,29 +29,27 @@
<!-- Matrix mit Buchstabenfeldern -->
<div class="d-flex justify-center mt-2 mb-2">
<div>
@foreach(var guess in PreviousGuesses)
@foreach (CharData[] guess in _previousGuesses)
{
<div class="d-flex mb-2">
@for(int i = 0; i < ColumnCount; i++)
@foreach (CharData cd in guess)
{
char guessedChar = char.ToUpper(guess[i]);
if(guessedChar == char.ToUpper(Secret[i]))
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">@guessedChar</MudText>
<MudText Typo="Typo.h3" Align="Align.Center">@cd.Char</MudText>
</MudPaper>
}
else if (Secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase))
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">@guessedChar</MudText>
<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">@guessedChar</MudText>
<MudText Typo="Typo.h3" Align="Align.Center">@cd.Char</MudText>
</MudPaper>
}
}
@ -48,99 +58,178 @@
</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" @onkeyup="@Check4Enter">
<MudContainer MaxWidth="MaxWidth.Small">
<MudGrid Spacing="2">
<MudItem xs="1"></MudItem>
<MudItem xs="1" />
<MudItem xs="8">
<MudTextField Class="" @ref="textField" T="string" FullWidth="true" @bind-Value="Input" Label="Dein Tipp:" Variant="Variant.Outlined" MaxLength="5" Mask="@InputMask">Hi</MudTextField>
<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=@ButtonOnClick>OK</MudButton>
<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"></MudItem>
<MudItem xs="1" />
</MudGrid>
</MudContainer>
@code {
public string Input { get; set; } = string.Empty;
public bool hideAlert = true;
private enum AlertState
{
None,
Correct,
Invalid
}
public MudTextField<string> textField;
public enum CharState
{
None,
Correct,
Misplaced,
Wrong
}
public List<string> PreviousGuesses = new();
public List<string> SecretList = new();
public struct CharData
{
public char Char;
public CharState State;
//Keine Ahnung wie das funktionert... Danke StackOverflow :)
/// <summary>
/// Eine Regexmaske die alle Strings matched die 0 bis 5 Buchstaben enthalten.
/// </summary>
//private IMask _inputMask = new RegexMask(@"^[A-Za-z]{0,5}$");
private bool found = false;
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; }
[Parameter]
public string? Secret { get; set; } = null;
[Parameter]
public IMask InputMask { get; set; } = new RegexMask(@"^[A-Za-z]{0,5}$");
[Parameter]
public Func<string, bool>? CheckInput { get; set; }
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>? GenerateSecret { get; set; }
public Func<string, bool> CheckInput { get; set; } = _ => true;
public void Check4Enter(KeyboardEventArgs e)
/// <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)
if(_found)
{
PlayAgain();
await PlayAgain();
}
else
{
ButtonOnClick();
await OnEnterGuessClick();
}
}
}
public void ButtonOnClick()
public async Task OnEnterGuessClick()
{
if (Input.Length != ColumnCount || found) return;
if (Input.Length != ColumnCount || _found) return;
if (CheckInput != null && !CheckInput(Input))
return; // TODO: Eingabe ungültig!
if (Secret == null && GenerateSecret != null)
Secret = GenerateSecret();
PreviousGuesses.Add(Input);
if(Secret!.Equals(Input, StringComparison.InvariantCultureIgnoreCase))
if (!CheckInput(Input))
{
found = true;
textField.Clear();
hideAlert = false;
_alert = AlertState.Invalid;
StateHasChanged();
return;
}
else
if (Secret == null)
await GenerateSecret.InvokeAsync();
if(await TestInput(Input))
{
textField.Clear();
_found = true;
_alert = AlertState.Correct;
}
await _textField.Clear();
}
public void PlayAgain()
/// <summary>
/// Prüft ob die Eingabe korrekt ist.
/// </summary>
private async Task<bool> TestInput(string input)
{
PreviousGuesses.Clear();
hideAlert = true;
found = false;
if (GenerateSecret != null)
Secret = GenerateSecret();
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();
}
}

View File

@ -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();
}
}

View File

@ -17,4 +17,8 @@
<PackageReference Include="MudBlazor" Version="6.0.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GottfriedsNackenUtility\GottfriedsNackenUtility.csproj" />
</ItemGroup>
</Project>

View File

@ -1,25 +1,50 @@
@page "/numberle"
@using GottfriedsNackenWebseite.Components
@using System.Diagnostics
@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="GenerateSecret" InputMask="@_inputMask" CheckInput="CheckInput" />
<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 IMask _inputMask = new RegexMask(@"^[0-9+\-*/=]{0,8}$");
private static readonly IMask _inputMask = new RegexMask(@"^[0-9+\-*/=]{0,8}$");
private Parser _parser = new();
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
@ -32,7 +57,7 @@
(decimal value, int equalSigns) = CrunchyValidaty(ast);
// Wir akzeptieren nur, wenn es exakt ein Gleichheitszeichen gibt und diese erfüllt ist.
// Wir akzeptieren nur, wenn es exakt ein Gleichheitszeichen gibt und die Gleichung erfüllt ist.
return value == 1 && equalSigns == 1;
}
catch
@ -80,12 +105,55 @@
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'
};
private string GenerateSecret()
/// <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;
@ -95,6 +163,8 @@
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';
@ -123,8 +193,26 @@
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);
}
}
}