From f51bd2bb44ad74ad376f36a85a900754bcd19f37 Mon Sep 17 00:00:00 2001 From: Gottfried Date: Tue, 19 Apr 2022 17:41:26 +0200 Subject: [PATCH] Start of Nacken Numberle --- .../Components/WordleComponent.razor | 146 ++++++++++++++++++ .../GottfriedsNackenWebseite/Numberle/Ast.cs | 31 ++++ .../Numberle/Parser.cs | 104 +++++++++++++ .../Numberle/Tokenizer.cs | 116 ++++++++++++++ .../Pages/Numberle.razor | 130 ++++++++++++++++ .../Pages/Wordle.razor | 29 ++-- .../Shared/NavMenu.razor | 3 + 7 files changed, 546 insertions(+), 13 deletions(-) create mode 100644 GottfriedsNackenWebseite/GottfriedsNackenWebseite/Components/WordleComponent.razor create mode 100644 GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Ast.cs create mode 100644 GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Parser.cs create mode 100644 GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Tokenizer.cs create mode 100644 GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Numberle.razor diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Components/WordleComponent.razor b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Components/WordleComponent.razor new file mode 100644 index 0000000..9db207b --- /dev/null +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Components/WordleComponent.razor @@ -0,0 +1,146 @@ + + + + +
+
+ @foreach(var guess in PreviousGuesses) + { +
+ @for(int i = 0; i < ColumnCount; i++) + { + char guessedChar = char.ToUpper(guess[i]); + + if(guessedChar == char.ToUpper(Secret[i])) + { + + @guessedChar + + } + else if (Secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase)) + { + + @guessedChar + + } + else + { + + @guessedChar + + } + } +
+ } +
+
+ + + + + + + Hi + + + OK + + + + + +@code { + public string Input { get; set; } = string.Empty; + + public bool hideAlert = true; + + public MudTextField textField; + + public List PreviousGuesses = new(); + public List SecretList = new(); + + //Keine Ahnung wie das funktionert... Danke StackOverflow :) + /// + /// Eine Regexmaske die alle Strings matched die 0 bis 5 Buchstaben enthalten. + /// + //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? CheckInput { get; set; } + + [Parameter] + public Func? 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(); + } +} diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Ast.cs b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Ast.cs new file mode 100644 index 0000000..c0577f2 --- /dev/null +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Ast.cs @@ -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; + } +} diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Parser.cs b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Parser.cs new file mode 100644 index 0000000..6b33488 --- /dev/null +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Parser.cs @@ -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 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; + } + + /// + /// EqualityExpression + /// : AdditiveExpression + /// | AdditiveExpression '=' EqualityExpression + /// ; + /// + private AstNode ParseEqualityExpression() + { + return ParseBinaryExpression(ParseAdditiveExpression, TokenType.Equals); + } + + /// + /// AdditiveExpression: + /// : MultiplicativeExpression + /// | MultiplicativeExpression ADDITIVE_OPERATOR AdditiveExpression + /// ; + /// + private AstNode ParseAdditiveExpression() + { + return ParseBinaryExpression(ParseMultiplicativeExpression, TokenType.AdditiveOperator); + } + + /// + /// MultiplicativeExpression: + /// : NumericLiteral + /// | MultiplicativeExpression MULTIPLICATIVE_OPERATOR NumericLiteral + /// ; + /// + private AstNode ParseMultiplicativeExpression() + { + return ParseBinaryExpression(ParseNumericLiteral, TokenType.MultiplicativeOperator); + } + + /// + /// AstNumericLiteral + /// : NumberLiteral + /// ; + /// + /// + 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; + } +} diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Tokenizer.cs b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Tokenizer.cs new file mode 100644 index 0000000..8916908 --- /dev/null +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Numberle/Tokenizer.cs @@ -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; + + /// + /// Gibt das aktuelle Zeichen der Eingabe zurück oder das Null-Zeichen ('\0') wenn das Ende der Eingabe erreicht wurde. + /// + 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; + } +} diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Numberle.razor b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Numberle.razor new file mode 100644 index 0000000..ee6cbde --- /dev/null +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Numberle.razor @@ -0,0 +1,130 @@ +@page "/numberle" + +@using GottfriedsNackenWebseite.Components +@using System.Diagnostics +@using GottfriedsNackenWebseite.Numberle + +Nacken Numberle + Gebe eine (korrekte) mathematische Gleichung ein: + + + +@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; + } + } + + /// + /// Berechnet den Wert der gegebenen (Teil-)Gleichung und zählt wie viele Gleichheitszeichen darin vorkommen. + /// + /// Die zu berechnende Gleichung. + /// Den Wert der Gleichung und die Anzahl Gleichheitszeichen + 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; + } + } + } +} diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Wordle.razor b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Wordle.razor index 2a5145a..84a2d1d 100644 --- a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Wordle.razor +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Pages/Wordle.razor @@ -25,28 +25,28 @@ @foreach(var guess in PreviousGuesses) {
- - @for(int i = 0; i < 5; i++) { - - var j = i; - if(@guess.ToUpper()[j] == secret.ToUpper()[j]) + char guessedChar = char.ToUpper(guess[i]); + + if(guessedChar == char.ToUpper(secret[i])) { - @guess.ToUpper()[j] + @guessedChar - }else if (secret.ToUpper().Contains(@guess.ToUpper()[j])) + } + else if (secret.Contains(guessedChar, StringComparison.InvariantCultureIgnoreCase)) { - @guess.ToUpper()[j] + @guessedChar - }else + } + else { - @guess.ToUpper()[j] + @guessedChar - } + } }
} @@ -58,7 +58,7 @@ - Hi + Hi OK @@ -78,7 +78,10 @@ public List SecretList = new List(); //Keine Ahnung wie das funktionert... Danke StackOverflow :) - public IMask mask = new RegexMask(@"^[A-Za-z]{0,5}$"); + /// + /// Eine Regexmaske die alle Strings matched die 0 bis 5 Buchstaben enthalten. + /// + private IMask _inputMask = new RegexMask(@"^[A-Za-z]{0,5}$"); public string secret = "penis"; private bool found = false; diff --git a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Shared/NavMenu.razor b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Shared/NavMenu.razor index abb6e79..c7e004f 100644 --- a/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Shared/NavMenu.razor +++ b/GottfriedsNackenWebseite/GottfriedsNackenWebseite/Shared/NavMenu.razor @@ -20,6 +20,9 @@ NackenDex + + Nacken Numberle + @*