Start of Nacken Numberle

This commit is contained in:
Gottfried Wilhelm Leibniz 2022-04-19 17:41:26 +02:00
parent 0d5961566b
commit f51bd2bb44
7 changed files with 546 additions and 13 deletions

View File

@ -0,0 +1,146 @@
<!-- Hidden Alerts -->
<div hidden="@hideAlert">
<MudContainer MaxWidth="MaxWidth.Small">
<MudGrid>
<MudItem xs="1"></MudItem>
<MudItem xs="8">
<MudAlert Severity="Severity.Success">Korrekt!</MudAlert>
</MudItem>
<MudItem xs="2">
<MudIconButton Icon="@Icons.Filled.Replay" Color="Color.Primary" aria-label="nochmal spielen" OnClick="@PlayAgain"></MudIconButton>
</MudItem>
<MudItem xs="1"></MudItem>
</MudGrid>
</MudContainer>
</div>
<!-- Matrix mit Buchstabenfeldern -->
<div class="d-flex justify-center mt-2 mb-2">
<div>
@foreach(var guess in PreviousGuesses)
{
<div class="d-flex mb-2">
@for(int i = 0; i < ColumnCount; i++)
{
char guessedChar = char.ToUpper(guess[i]);
if(guessedChar == char.ToUpper(Secret[i]))
{
<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>
</MudPaper>
}
else if (Secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase))
{
<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>
</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>
</MudPaper>
}
}
</div>
}
</div>
</div>
<!-- Input dialogue feld -->
<MudContainer MaxWidth="MaxWidth.Small" @onkeyup="@Check4Enter">
<MudGrid Spacing="2">
<MudItem xs="1"></MudItem>
<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>
</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>
</MudItem>
<MudItem xs="1"></MudItem>
</MudGrid>
</MudContainer>
@code {
public string Input { get; set; } = string.Empty;
public bool hideAlert = true;
public MudTextField<string> textField;
public List<string> PreviousGuesses = new();
public List<string> SecretList = new();
//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;
[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; }
[Parameter]
public Func<string>? GenerateSecret { get; set; }
public void Check4Enter(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
if(found)
{
PlayAgain();
}
else
{
ButtonOnClick();
}
}
}
public void ButtonOnClick()
{
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))
{
found = true;
textField.Clear();
hideAlert = false;
}
else
{
textField.Clear();
}
}
public void PlayAgain()
{
PreviousGuesses.Clear();
hideAlert = true;
found = false;
if (GenerateSecret != null)
Secret = GenerateSecret();
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
@page "/numberle"
@using GottfriedsNackenWebseite.Components
@using System.Diagnostics
@using GottfriedsNackenWebseite.Numberle
<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" />
@code
{
private IMask _inputMask = new RegexMask(@"^[0-9+\-*/=]{0,8}$");
private Parser _parser = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
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 diese 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);
}
private static readonly char[] Chars = {
'=', '+', '-', '*', '/',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
};
private string GenerateSecret()
{
int charCount = 8;
char[] formula = new char[charCount];
// Gleichzeichen plazieren
int equalsIndex = Random.Shared.Next(1, charCount - 1);
formula[equalsIndex] = '=';
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;
}
}
}
}

View File

@ -25,28 +25,28 @@
@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>
} }
} }
</div> </div>
} }
@ -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;

View File

@ -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">