From 215af218e589d247c3d4dfa2de5714828b9e0536 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 26 Jan 2022 22:11:01 +1100 Subject: [PATCH 01/21] Add dotnet Common library solution --- .../Games.Common.Sample.csproj | 13 +++++++ .../Games.Common.Test.csproj | 27 +++++++++++++++ 00_Common/dotnet/Games.Common.sln | 34 +++++++++++++++++++ .../dotnet/Games.Common/Games.Common.csproj | 7 ++++ 4 files changed, 81 insertions(+) create mode 100644 00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj create mode 100644 00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj create mode 100644 00_Common/dotnet/Games.Common.sln create mode 100644 00_Common/dotnet/Games.Common/Games.Common.csproj diff --git a/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj b/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj new file mode 100644 index 00000000..a4eb9b00 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj @@ -0,0 +1,13 @@ + + + + + + + + Exe + net6.0 + enable + + + diff --git a/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj b/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj new file mode 100644 index 00000000..6fe85680 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/00_Common/dotnet/Games.Common.sln b/00_Common/dotnet/Games.Common.sln new file mode 100644 index 00000000..3498c7d1 --- /dev/null +++ b/00_Common/dotnet/Games.Common.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common", "Games.Common\Games.Common.csproj", "{005F2A3E-4E45-418B-8D19-E735B3BD4535}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common.Test", "Games.Common.Test\Games.Common.Test.csproj", "{8369DA66-0414-4A14-B5BE-73B0159498A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common.Sample", "Games.Common.Sample\Games.Common.Sample.csproj", "{395FBF0D-404E-495B-9760-8BEE3A6F5B62}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {005F2A3E-4E45-418B-8D19-E735B3BD4535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {005F2A3E-4E45-418B-8D19-E735B3BD4535}.Debug|Any CPU.Build.0 = Debug|Any CPU + {005F2A3E-4E45-418B-8D19-E735B3BD4535}.Release|Any CPU.ActiveCfg = Release|Any CPU + {005F2A3E-4E45-418B-8D19-E735B3BD4535}.Release|Any CPU.Build.0 = Release|Any CPU + {8369DA66-0414-4A14-B5BE-73B0159498A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8369DA66-0414-4A14-B5BE-73B0159498A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8369DA66-0414-4A14-B5BE-73B0159498A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8369DA66-0414-4A14-B5BE-73B0159498A2}.Release|Any CPU.Build.0 = Release|Any CPU + {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/00_Common/dotnet/Games.Common/Games.Common.csproj b/00_Common/dotnet/Games.Common/Games.Common.csproj new file mode 100644 index 00000000..9f5c4f4a --- /dev/null +++ b/00_Common/dotnet/Games.Common/Games.Common.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + From 25c8dad5127899ec9c08eac9d4662e19ebba6240 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 7 Feb 2022 22:43:31 +1100 Subject: [PATCH 02/21] Add input tokenization --- 00_Common/dotnet/Directory.Build.props | 8 ++ .../Games.Common.Test.csproj | 3 +- .../TextIOTests/TokenizerTests.cs | 33 +++++++ .../dotnet/Games.Common/Games.Common.csproj | 2 +- 00_Common/dotnet/Games.Common/IO/Token.cs | 37 ++++++++ 00_Common/dotnet/Games.Common/IO/Tokenizer.cs | 90 +++++++++++++++++++ .../Games.Common/_InternalsVisibleTo.cs | 3 + 7 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 00_Common/dotnet/Directory.Build.props create mode 100644 00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs create mode 100644 00_Common/dotnet/Games.Common/IO/Token.cs create mode 100644 00_Common/dotnet/Games.Common/IO/Tokenizer.cs create mode 100644 00_Common/dotnet/Games.Common/_InternalsVisibleTo.cs diff --git a/00_Common/dotnet/Directory.Build.props b/00_Common/dotnet/Directory.Build.props new file mode 100644 index 00000000..f5a054dc --- /dev/null +++ b/00_Common/dotnet/Directory.Build.props @@ -0,0 +1,8 @@ + + + + enable + 10.0 + + + diff --git a/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj b/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj index 6fe85680..c90ed518 100644 --- a/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj +++ b/00_Common/dotnet/Games.Common.Test/Games.Common.Test.csproj @@ -2,12 +2,11 @@ net6.0 - enable - false + diff --git a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs new file mode 100644 index 00000000..4e9c9e84 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs @@ -0,0 +1,33 @@ +using FluentAssertions; +using Xunit; + +namespace Games.Common.IO +{ + public class TokenizerTests + { + [Theory] + [MemberData(nameof(TokenizerTestCases))] + public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected) + { + var result = Tokenizer.ParseTokens(input); + + result.Should().BeEquivalentTo(expected); + } + + public static TheoryData TokenizerTestCases() => new() + { + { "", new[] { "" } }, + { "aBc", new[] { "aBc" } }, + { " Foo ", new[] { "Foo" } }, + { " \" Foo \" ", new[] { " Foo " } }, + { " \" Foo ", new[] { " Foo " } }, + { "\"\"abc", new[] { "" } }, + { "a\"\"bc", new[] { "a\"\"bc" } }, + { "\"\"", new[] { "" } }, + { ",", new[] { "", "" } }, + { " foo ,bar", new[] { "foo", "bar" } }, + { "\"\"bc,de", new[] { "", "de" } }, + { "a\"b,\" c,d\"e, f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } + }; + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/Games.Common.csproj b/00_Common/dotnet/Games.Common/Games.Common.csproj index 9f5c4f4a..d4c395e8 100644 --- a/00_Common/dotnet/Games.Common/Games.Common.csproj +++ b/00_Common/dotnet/Games.Common/Games.Common.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs new file mode 100644 index 00000000..3f8267cb --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -0,0 +1,37 @@ +using System.Text; + +namespace Games.Common.IO +{ + internal class Token + { + protected readonly StringBuilder _builder; + private int _trailingWhiteSpaceCount; + + private Token() + { + _builder = new StringBuilder(); + } + + public Token Append(char character) + { + _builder.Append(character); + + _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; + + return this; + } + + public override string ToString() => _builder.ToString(0, _builder.Length - _trailingWhiteSpaceCount); + + public static Token Create() => new(); + + public static Token CreateQuoted() => new QuotedToken(); + + public static implicit operator string(Token token) => token.ToString(); + + internal class QuotedToken : Token + { + public override string ToString() => _builder.ToString(); + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs new file mode 100644 index 00000000..9a5d43a4 --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; + +namespace Games.Common.IO +{ + internal class Tokenizer + { + private const char Quote = '"'; + private const char Separator = ','; + + private readonly Queue _characters; + + private Tokenizer(string input) => _characters = new Queue(input); + + public static IEnumerable ParseTokens(string input) + { + if (input is null) { throw new ArgumentNullException(nameof(input)); } + + return new Tokenizer(input).ParseTokens(); + } + + private IEnumerable ParseTokens() + { + while (true) + { + var (token, isLastToken) = Consume(_characters); + yield return token; + + if (isLastToken) { break; } + } + } + + public (Token, bool) Consume(Queue characters) + { + var token = Token.Create(); + var state = ITokenizerState.LookForStartOfToken; + + while (characters.TryDequeue(out var character)) + { + (state, token) = state.Consume(character, token); + if (state is AtEndOfTokenState) { return (token, false); } + } + + return (token, true); + } + + private interface ITokenizerState + { + public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); + + (ITokenizerState, Token) Consume(char character, Token token); + } + + private struct LookForStartOfTokenState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => + character switch + { + Separator => (new AtEndOfTokenState(), token), + Quote => (new InQuotedTokenState(), Token.CreateQuoted()), + _ when char.IsWhiteSpace(character) => (this, token), + _ => (new InTokenState(), token.Append(character)) + }; + } + + private struct InTokenState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => + character == Separator ? (new AtEndOfTokenState(), token) : (this, token.Append(character)); + } + + private struct InQuotedTokenState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => + character == Quote ? (new LookForSeparatorState(), token) : (this, token.Append(character)); + } + + private struct LookForSeparatorState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => + (character == Separator ? new AtEndOfTokenState() : this, token); + } + + private struct AtEndOfTokenState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => + throw new InvalidOperationException(); + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/_InternalsVisibleTo.cs b/00_Common/dotnet/Games.Common/_InternalsVisibleTo.cs new file mode 100644 index 00000000..2ffc2ca3 --- /dev/null +++ b/00_Common/dotnet/Games.Common/_InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Games.Common.Test")] \ No newline at end of file From 3b42ffd18dcdf2d14bef67e9908719281b029dc3 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Thu, 10 Feb 2022 22:40:12 +1100 Subject: [PATCH 03/21] Add token reader --- .../TextIOTests/TokenReaderTests.cs | 88 +++++++++++++++++++ .../TextIOTests/TokenizerTests.cs | 7 +- .../dotnet/Games.Common/IO/TokenReader.cs | 75 ++++++++++++++++ 00_Common/dotnet/Games.Common/IO/Tokenizer.cs | 15 ++-- 4 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs create mode 100644 00_Common/dotnet/Games.Common/IO/TokenReader.cs diff --git a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs new file mode 100644 index 00000000..34443dd2 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Linq; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +using static System.Environment; +using TwoStrings = System.ValueTuple; + +namespace Games.Common.IO +{ + public class TokenReaderTests + { + private readonly StringWriter _outputWriter; + + public TokenReaderTests() + { + _outputWriter = new StringWriter(); + } + + [Fact] + public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() + { + var sut = CreateTokenReader(""); + + Action readTokens = () => sut.ReadTokens("", 0); + + readTokens.Should().Throw() + .WithMessage("'quantityNeeded' must be greater than zero.*") + .WithParameterName("quantityNeeded"); + } + + + [Theory] + [MemberData(nameof(ReadTokensTestCases))] + public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + T[] expectedResult) + { + var sut = CreateTokenReader(input); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.ToString()).Should().BeEquivalentTo(expectedResult); + } + + private TokenReader CreateTokenReader(string input) => + new TokenReader( + new TextIO( + new StringReader(input + NewLine), + _outputWriter)); + + public static TheoryData ReadTokensTestCases() + { + return new() + { + { "Name", 1, "Bill", "Name? ", new[] { "Bill" } }, + { "Names", 2, " Bill , Bloggs ", "Names? ", new[] { "Bill", "Bloggs" } }, + { "Names", 2, $" Bill{NewLine}Bloggs ", "Names? ?? ", new[] { "Bill", "Bloggs" } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", + $"Foo? ?? ?? ?? !Extra input ingored{NewLine}", + new[] { "1", "2", " a,b ", "", "", "d\"x" } + } + }; + } + + public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + { + static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); + + return new() + { + { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, + { Read2Strings("Input please"), "aBc , DeF ", "Input please? ", ("aBc", "DeF") }, + }; + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs index 4e9c9e84..a4e2f762 100644 --- a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs +++ b/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs @@ -1,3 +1,4 @@ +using System.Linq; using FluentAssertions; using Xunit; @@ -11,7 +12,7 @@ namespace Games.Common.IO { var result = Tokenizer.ParseTokens(input); - result.Should().BeEquivalentTo(expected); + result.Select(t => t.ToString()).Should().BeEquivalentTo(expected); } public static TheoryData TokenizerTestCases() => new() @@ -26,8 +27,8 @@ namespace Games.Common.IO { "\"\"", new[] { "" } }, { ",", new[] { "", "" } }, { " foo ,bar", new[] { "foo", "bar" } }, - { "\"\"bc,de", new[] { "", "de" } }, - { "a\"b,\" c,d\"e, f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } + { "\"a\"bc,de", new[] { "a" } }, + { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } }; } } \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs new file mode 100644 index 00000000..0045433d --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +namespace Games.Common.IO +{ + internal class TokenReader + { + private readonly TextIO _io; + private readonly Func _isTokenValid; + + public TokenReader(TextIO io, Func? isTokenValid = null) + { + _io = io; + _isTokenValid = isTokenValid ?? (t => true); + } + + public IEnumerable ReadTokens(string prompt, uint quantityNeeded) + { + if (quantityNeeded == 0) + { + throw new ArgumentOutOfRangeException( + nameof(quantityNeeded), + $"'{nameof(quantityNeeded)}' must be greater than zero."); + } + + var tokens = new List(); + + while(tokens.Count < quantityNeeded) + { + tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count)); + prompt = "?"; + } + + return tokens; + } + + private IEnumerable ReadValidTokens(string prompt, uint maxCount) + { + while (true) + { + var tokensValid = true; + var tokens = new List(); + foreach (var token in ReadLineOfTokens(prompt, maxCount)) + { + if (!_isTokenValid(token)) + { + tokensValid = false; + prompt = "?"; + break; + } + + tokens.Add(token); + } + + if (tokensValid) { return tokens; } + } + } + + private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) + { + var tokenCount = 0; + + foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt))) + { + if (++tokenCount > maxCount) + { + _io.WriteLine("!Extra input ingored"); + break; + } + + yield return token; + } + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs index 9a5d43a4..14900eb1 100644 --- a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -12,14 +12,14 @@ namespace Games.Common.IO private Tokenizer(string input) => _characters = new Queue(input); - public static IEnumerable ParseTokens(string input) + public static IEnumerable ParseTokens(string input) { if (input is null) { throw new ArgumentNullException(nameof(input)); } return new Tokenizer(input).ParseTokens(); } - private IEnumerable ParseTokens() + private IEnumerable ParseTokens() { while (true) { @@ -72,13 +72,18 @@ namespace Games.Common.IO private struct InQuotedTokenState : ITokenizerState { public (ITokenizerState, Token) Consume(char character, Token token) => - character == Quote ? (new LookForSeparatorState(), token) : (this, token.Append(character)); + character == Quote ? (new ExpectSeparatorState(), token) : (this, token.Append(character)); } - private struct LookForSeparatorState : ITokenizerState + private struct ExpectSeparatorState : ITokenizerState { public (ITokenizerState, Token) Consume(char character, Token token) => - (character == Separator ? new AtEndOfTokenState() : this, token); + character == Separator ? (new AtEndOfTokenState(), token) : (new IgnoreRestOfLineState(), token); + } + + private struct IgnoreRestOfLineState : ITokenizerState + { + public (ITokenizerState, Token) Consume(char character, Token token) => (this, token); } private struct AtEndOfTokenState : ITokenizerState From a7cedfbf7e5c32f40573690d572c26a4b6defec7 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Fri, 11 Feb 2022 08:19:02 +1100 Subject: [PATCH 04/21] Rework token building --- 00_Common/dotnet/Games.Common/IO/Token.cs | 43 +++++++++++-------- 00_Common/dotnet/Games.Common/IO/Tokenizer.cs | 43 +++++++++++-------- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs index 3f8267cb..4faea1fc 100644 --- a/00_Common/dotnet/Games.Common/IO/Token.cs +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -4,34 +4,41 @@ namespace Games.Common.IO { internal class Token { - protected readonly StringBuilder _builder; - private int _trailingWhiteSpaceCount; + private readonly string _value; - private Token() + private Token(string value) { - _builder = new StringBuilder(); + _value = value; } - public Token Append(char character) + public override string ToString() => _value; + + internal class Builder { - _builder.Append(character); + private readonly StringBuilder _builder = new(); + private bool _isQuoted; + private int _trailingWhiteSpaceCount; - _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; + public Builder Append(char character) + { + _builder.Append(character); - return this; - } + _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; - public override string ToString() => _builder.ToString(0, _builder.Length - _trailingWhiteSpaceCount); + return this; + } - public static Token Create() => new(); + public Builder SetIsQuoted() + { + _isQuoted = true; + return this; + } - public static Token CreateQuoted() => new QuotedToken(); - - public static implicit operator string(Token token) => token.ToString(); - - internal class QuotedToken : Token - { - public override string ToString() => _builder.ToString(); + public Token Build() + { + if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; } + return new Token(_builder.ToString()); + } } } } \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs index 14900eb1..857b3331 100644 --- a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -32,63 +32,70 @@ namespace Games.Common.IO public (Token, bool) Consume(Queue characters) { - var token = Token.Create(); + var tokenBuilder = new Token.Builder(); var state = ITokenizerState.LookForStartOfToken; while (characters.TryDequeue(out var character)) { - (state, token) = state.Consume(character, token); - if (state is AtEndOfTokenState) { return (token, false); } + (state, tokenBuilder) = state.Consume(character, tokenBuilder); + if (state is AtEndOfTokenState) { return (tokenBuilder.Build(), false); } } - return (token, true); + return (tokenBuilder.Build(), true); } private interface ITokenizerState { public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); - (ITokenizerState, Token) Consume(char character, Token token); + (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder); } private struct LookForStartOfTokenState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => character switch { - Separator => (new AtEndOfTokenState(), token), - Quote => (new InQuotedTokenState(), Token.CreateQuoted()), - _ when char.IsWhiteSpace(character) => (this, token), - _ => (new InTokenState(), token.Append(character)) + Separator => (new AtEndOfTokenState(), tokenBuilder), + Quote => (new InQuotedTokenState(), tokenBuilder.SetIsQuoted()), + _ when char.IsWhiteSpace(character) => (this, tokenBuilder), + _ => (new InTokenState(), tokenBuilder.Append(character)) }; } private struct InTokenState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => - character == Separator ? (new AtEndOfTokenState(), token) : (this, token.Append(character)); + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); } private struct InQuotedTokenState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => - character == Quote ? (new ExpectSeparatorState(), token) : (this, token.Append(character)); + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Quote + ? (new ExpectSeparatorState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); } private struct ExpectSeparatorState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => - character == Separator ? (new AtEndOfTokenState(), token) : (new IgnoreRestOfLineState(), token); + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (new IgnoreRestOfLineState(), tokenBuilder); } private struct IgnoreRestOfLineState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => (this, token); + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + (this, tokenBuilder); } private struct AtEndOfTokenState : ITokenizerState { - public (ITokenizerState, Token) Consume(char character, Token token) => + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => throw new InvalidOperationException(); } } From 151144e9e24313308de2714073d4b67ea156a4ec Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Fri, 11 Feb 2022 20:45:01 +1100 Subject: [PATCH 05/21] Reorganise test files --- .../Games.Common.Test/{TextIOTests => IO}/TokenReaderTests.cs | 0 .../Games.Common.Test/{TextIOTests => IO}/TokenizerTests.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename 00_Common/dotnet/Games.Common.Test/{TextIOTests => IO}/TokenReaderTests.cs (100%) rename 00_Common/dotnet/Games.Common.Test/{TextIOTests => IO}/TokenizerTests.cs (100%) diff --git a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs similarity index 100% rename from 00_Common/dotnet/Games.Common.Test/TextIOTests/TokenReaderTests.cs rename to 00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs diff --git a/00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs similarity index 100% rename from 00_Common/dotnet/Games.Common.Test/TextIOTests/TokenizerTests.cs rename to 00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs From 8d04213ccd7275777cfebcf9bdc0897a2d9c26a6 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Fri, 11 Feb 2022 22:16:03 +1100 Subject: [PATCH 06/21] Add reading of number tokens --- .../Games.Common.Test/IO/TokenReaderTests.cs | 55 +++++++++++++------ .../dotnet/Games.Common.Test/IO/TokenTests.cs | 43 +++++++++++++++ 00_Common/dotnet/Games.Common/IO/Token.cs | 24 ++++++-- .../dotnet/Games.Common/IO/TokenReader.cs | 17 ++++-- 4 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs index 34443dd2..3a4b9e9b 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs @@ -6,12 +6,14 @@ using FluentAssertions.Execution; using Xunit; using static System.Environment; -using TwoStrings = System.ValueTuple; namespace Games.Common.IO { public class TokenReaderTests { + const string NumberExpected = "!Number expected - retry input line"; + const string ExtraInput = "!Extra input ignored"; + private readonly StringWriter _outputWriter; public TokenReaderTests() @@ -22,7 +24,7 @@ namespace Games.Common.IO [Fact] public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() { - var sut = CreateTokenReader(""); + var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); Action readTokens = () => sut.ReadTokens("", 0); @@ -34,28 +36,41 @@ namespace Games.Common.IO [Theory] [MemberData(nameof(ReadTokensTestCases))] - public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( + public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( string prompt, uint tokenCount, string input, string expectedOutput, - T[] expectedResult) + string[] expectedResult) { - var sut = CreateTokenReader(input); + var sut = TokenReader.ForStrings(new TextIO(new StringReader(input + NewLine), _outputWriter)); var result = sut.ReadTokens(prompt, tokenCount); var output = _outputWriter.ToString(); using var _ = new AssertionScope(); output.Should().Be(expectedOutput); - result.Select(t => t.ToString()).Should().BeEquivalentTo(expectedResult); + result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); } - private TokenReader CreateTokenReader(string input) => - new TokenReader( - new TextIO( - new StringReader(input + NewLine), - _outputWriter)); + [Theory] + [MemberData(nameof(ReadNumericTokensTestCases))] + public void ReadTokens_Numeric_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + float[] expectedResult) + { + var sut = TokenReader.ForNumbers(new TextIO(new StringReader(input + NewLine), _outputWriter)); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.Number).Should().BeEquivalentTo(expectedResult); + } public static TheoryData ReadTokensTestCases() { @@ -68,20 +83,26 @@ namespace Games.Common.IO "Foo", 6, $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", - $"Foo? ?? ?? ?? !Extra input ingored{NewLine}", + $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", new[] { "1", "2", " a,b ", "", "", "d\"x" } } }; } - public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + public static TheoryData ReadNumericTokensTestCases() { - static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); - return new() { - { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, - { Read2Strings("Input please"), "aBc , DeF ", "Input please? ", ("aBc", "DeF") }, + { "Age", 1, "23", "Age? ", new[] { 23F } }, + { "Constants", 2, " 3.141 , 2.71 ", "Constants? ", new[] { 3.141F, 2.71F } }, + { "Answer", 1, $"Forty-two{NewLine}42 ", $"Answer? {NumberExpected}{NewLine}? ", new[] { 42F } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine}3, 4 {NewLine}5.6,7,a, b", + $"Foo? ?? {NumberExpected}{NewLine}? ?? {ExtraInput}{NewLine}", + new[] { 1, 2, 3, 4, 5.6F, 7 } + } }; } } diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs new file mode 100644 index 00000000..f716ca78 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using Xunit; + +namespace Games.Common.IO +{ + public class TokenTests + { + [Theory] + [MemberData(nameof(TokenTestCases))] + public void Ctor_PopulatesProperties(string value, bool isNumber, float number) + { + var expected = new { String = value, IsNumber = isNumber, Number = number }; + + var token = new Token(value); + + token.Should().BeEquivalentTo(expected); + } + + public static TheoryData TokenTestCases() => new() + { + { "", false, float.NaN }, + { "abcde", false, float.NaN }, + { "123 ", true, 123 }, + { "+42 ", true, 42 }, + { "-42 ", true, -42 }, + { "+3.14159 ", true, 3.14159F }, + { "-3.14159 ", true, -3.14159F }, + { " 123", false, float.NaN }, + { "1.2e4", true, 12000 }, + { "2.3e-5", true, 0.000023F }, + { "1e100", true, float.MaxValue }, + { "-1E100", true, float.MinValue }, + { "1E-100", true, 0 }, + { "-1e-100", true, 0 }, + { "100abc", true, 100 }, + { "1,2,3", true, 1 }, + { "42,a,b", true, 42 }, + { "1.2.3", true, 1.2F }, + { "12e.5", false, float.NaN }, + { "12e0.5", true, 12 } + }; + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs index 4faea1fc..f1520c8e 100644 --- a/00_Common/dotnet/Games.Common/IO/Token.cs +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -1,17 +1,33 @@ using System.Text; +using System.Text.RegularExpressions; namespace Games.Common.IO { internal class Token { - private readonly string _value; + private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); - private Token(string value) + internal Token(string value) { - _value = value; + String = value; + + var match = _numberPattern.Match(String); + + IsNumber = float.TryParse(match.Value, out var number); + Number = (IsNumber, number) switch + { + (false, _) => float.NaN, + (true, float.PositiveInfinity) => float.MaxValue, + (true, float.NegativeInfinity) => float.MinValue, + (true, _) => number + }; } - public override string ToString() => _value; + public string String { get; } + public bool IsNumber { get; } + public float Number { get; } + + public override string ToString() => String; internal class Builder { diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index 0045433d..f2046dc3 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -5,15 +5,21 @@ namespace Games.Common.IO { internal class TokenReader { - private readonly TextIO _io; - private readonly Func _isTokenValid; + private const string NumberExpected = "!Number expected - retry input line"; + private const string ExtraInput = "!Extra input ignored"; - public TokenReader(TextIO io, Func? isTokenValid = null) + private readonly TextIO _io; + private readonly Predicate _isTokenValid; + + private TokenReader(TextIO io, Predicate isTokenValid) { _io = io; _isTokenValid = isTokenValid ?? (t => true); } + public static TokenReader ForStrings(TextIO io) => new(io, t => true); + public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + public IEnumerable ReadTokens(string prompt, uint quantityNeeded) { if (quantityNeeded == 0) @@ -44,8 +50,9 @@ namespace Games.Common.IO { if (!_isTokenValid(token)) { + _io.WriteLine(NumberExpected); tokensValid = false; - prompt = "?"; + prompt = ""; break; } @@ -64,7 +71,7 @@ namespace Games.Common.IO { if (++tokenCount > maxCount) { - _io.WriteLine("!Extra input ingored"); + _io.WriteLine(ExtraInput); break; } From d8e4e0d975b8c3c7e7ae6a20340266b796994d09 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 21:57:21 +1100 Subject: [PATCH 07/21] Extract IO string constants --- 00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs | 4 +--- 00_Common/dotnet/Games.Common/IO/Strings.cs | 8 ++++++++ 00_Common/dotnet/Games.Common/IO/TokenReader.cs | 5 ++--- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 00_Common/dotnet/Games.Common/IO/Strings.cs diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs index 3a4b9e9b..65b33908 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs @@ -6,14 +6,12 @@ using FluentAssertions.Execution; using Xunit; using static System.Environment; +using static Games.Common.IO.Strings; namespace Games.Common.IO { public class TokenReaderTests { - const string NumberExpected = "!Number expected - retry input line"; - const string ExtraInput = "!Extra input ignored"; - private readonly StringWriter _outputWriter; public TokenReaderTests() diff --git a/00_Common/dotnet/Games.Common/IO/Strings.cs b/00_Common/dotnet/Games.Common/IO/Strings.cs new file mode 100644 index 00000000..3d955409 --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/Strings.cs @@ -0,0 +1,8 @@ +namespace Games.Common.IO +{ + internal static class Strings + { + internal const string NumberExpected = "!Number expected - retry input line"; + internal const string ExtraInput = "!Extra input ignored"; + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index f2046dc3..b7eb3774 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -1,13 +1,12 @@ using System; using System.Collections.Generic; +using static Games.Common.IO.Strings; + namespace Games.Common.IO { internal class TokenReader { - private const string NumberExpected = "!Number expected - retry input line"; - private const string ExtraInput = "!Extra input ignored"; - private readonly TextIO _io; private readonly Predicate _isTokenValid; From 6f20449e71d5589c2d1dc48b53e53214a4910bc3 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 22:11:35 +1100 Subject: [PATCH 08/21] Add IReadWrite interface --- .../dotnet/Games.Common/IO/IReadWrite.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 00_Common/dotnet/Games.Common/IO/IReadWrite.cs diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs new file mode 100644 index 00000000..883e069d --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -0,0 +1,69 @@ +namespace Games.Common.IO +{ + /// + /// Provides for input and output of strings and numbers. + /// + public interface IReadWrite + { + /// + /// Reads a value from input. + /// + /// The text to display to prompt for the value. + /// A , being the value entered. + float ReadNumber(string prompt); + + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float) Read2Numbers(string prompt); + + /// + /// Reads 3 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float) Read3Numbers(string prompt); + + /// + /// Reads 4 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float, float) Read4Numbers(string prompt); + + /// + /// Read numbers from input to fill an array. + /// + /// The text to display to prompt for the values. + /// A to be filled with values from input. + void ReadNumbers(string prompt, float[] numbers); + + /// + /// Reads a value from input. + /// + /// The text to display to prompt for the value. + /// A , being the value entered. + string ReadString(string prompt); + + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (string, string) Read2Strings(string prompt); + + /// + /// Writes a to output. + /// + /// The to be written. + void Write(string message); + + /// + /// Writes a to output, followed by a new-line. + /// + /// The to be written. + void WriteLine(string message); + } +} \ No newline at end of file From ee84b19150599744d2e0f3334c041dcadf58a711 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 22:26:12 +1100 Subject: [PATCH 09/21] Add TextIO implemnetation of IReadWrite --- .../IO/TextIOTests/ReadMethodTests.cs | 166 ++++++++++++++++++ .../dotnet/Games.Common/IO/IReadWrite.cs | 4 +- 00_Common/dotnet/Games.Common/IO/TextIO.cs | 90 ++++++++++ 3 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs create mode 100644 00_Common/dotnet/Games.Common/IO/TextIO.cs diff --git a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs new file mode 100644 index 00000000..3b5de46d --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using FluentAssertions.Execution; +using Xunit; + +using TwoStrings = System.ValueTuple; +using TwoNumbers = System.ValueTuple; +using ThreeNumbers = System.ValueTuple; +using FourNumbers = System.ValueTuple; + +using static System.Environment; +using static Games.Common.IO.Strings; + +namespace Games.Common.IO.TextIOTests +{ + public class ReadMethodTests + { + [Theory] + [MemberData(nameof(ReadStringTestCases))] + [MemberData(nameof(Read2StringsTestCases))] + [MemberData(nameof(ReadNumberTestCases))] + [MemberData(nameof(Read2NumbersTestCases))] + [MemberData(nameof(Read3NumbersTestCases))] + [MemberData(nameof(Read4NumbersTestCases))] + [MemberData(nameof(ReadNumbersTestCases))] + public void ReadingValuesHasExpectedPromptsAndResults( + Func read, + string input, + string expectedOutput, + T expectedResult) + { + var inputReader = new StringReader(input + Environment.NewLine); + var outputWriter = new StringWriter(); + var io = new TextIO(inputReader, outputWriter); + + var result = read.Invoke(io); + var output = outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Should().BeEquivalentTo(expectedResult); + } + + [Fact] + public void ReadNumbers_ArrayEmpty_ThrowsArgumentException() + { + var io = new TextIO(new StringReader(""), new StringWriter()); + + Action readNumbers = () => io.ReadNumbers("foo", Array.Empty()); + + readNumbers.Should().Throw() + .WithMessage("'values' must have a non-zero length.*") + .WithParameterName("values"); + } + + public static TheoryData, string, string, string> ReadStringTestCases() + { + static Func ReadString(string prompt) => io => io.ReadString(prompt); + + return new() + { + { ReadString("Name"), "", "Name? ", "" }, + { ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" } + }; + } + + public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + { + static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); + + return new() + { + { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, + { + Read2Strings("Input please"), + $"{NewLine}x,y", + $"Input please? ?? {ExtraInput}{NewLine}", + ("", "x") + } + }; + } + + public static TheoryData, string, string, float> ReadNumberTestCases() + { + static Func ReadNumber(string prompt) => io => io.ReadNumber(prompt); + + return new() + { + { ReadNumber("Age"), $"{NewLine}42,", $"Age? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", 42 }, + { ReadNumber("Guess"), "3,4,5", $"Guess? {ExtraInput}{NewLine}", 3 } + }; + } + + public static TheoryData, string, string, TwoNumbers> Read2NumbersTestCases() + { + static Func Read2Numbers(string prompt) => io => io.Read2Numbers(prompt); + + return new() + { + { Read2Numbers("Point"), "3,4,5", $"Point? {ExtraInput}{NewLine}", (3, 4) }, + { + Read2Numbers("Foo"), + $"x,4,5{NewLine}4,5,x", + $"Foo? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5) + } + }; + } + + public static TheoryData, string, string, ThreeNumbers> Read3NumbersTestCases() + { + static Func Read3Numbers(string prompt) => io => io.Read3Numbers(prompt); + + return new() + { + { Read3Numbers("Point"), "3.2, 4.3, 5.4, 6.5", $"Point? {ExtraInput}{NewLine}", (3.2F, 4.3F, 5.4F) }, + { + Read3Numbers("Bar"), + $"x,4,5{NewLine}4,5,x{NewLine}6,7,8,y", + $"Bar? {NumberExpected}{NewLine}? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (6, 7, 8) + } + }; + } + + public static TheoryData, string, string, FourNumbers> Read4NumbersTestCases() + { + static Func Read4Numbers(string prompt) => io => io.Read4Numbers(prompt); + + return new() + { + { Read4Numbers("Point"), "3,4,5,6,7", $"Point? {ExtraInput}{NewLine}", (3, 4, 5, 6) }, + { + Read4Numbers("Baz"), + $"x,4,5,6{NewLine} 4, 5 , 6,7 ,x", + $"Baz? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5, 6, 7) + } + }; + } + + public static TheoryData>, string, string, float[]> ReadNumbersTestCases() + { + static Func> ReadNumbers(string prompt) => + io => + { + var numbers = new float[6]; + io.ReadNumbers(prompt, numbers); + return numbers; + }; + + return new() + { + { ReadNumbers("Primes"), "2, 3, 5, 7, 11, 13", $"Primes? ", new float[] { 2, 3, 5, 7, 11, 13 } }, + { + ReadNumbers("Qux"), + $"42{NewLine}3.141, 2.718{NewLine}3.0e8, 6.02e23{NewLine}9.11E-28", + $"Qux? ?? ?? ?? ", + new[] { 42, 3.141F, 2.718F, 3.0e8F, 6.02e23F, 9.11E-28F } + } + }; + } + } +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs index 883e069d..c5d24101 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -37,8 +37,8 @@ namespace Games.Common.IO /// Read numbers from input to fill an array. /// /// The text to display to prompt for the values. - /// A to be filled with values from input. - void ReadNumbers(string prompt, float[] numbers); + /// A to be filled with values from input. + void ReadNumbers(string prompt, float[] values); /// /// Reads a value from input. diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs new file mode 100644 index 00000000..e210ec4e --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Games.Common.IO +{ + /// + /// + /// Implements with input read from a and output written to a + /// . + /// + public class TextIO : IReadWrite + { + private readonly TextReader _input; + private readonly TextWriter _output; + private readonly TokenReader _stringTokenReader; + private readonly TokenReader _numberTokenReader; + + public TextIO(TextReader input, TextWriter output) + { + _input = input ?? throw new ArgumentNullException(nameof(input)); + _output = output ?? throw new ArgumentNullException(nameof(output)); + _stringTokenReader = TokenReader.ForStrings(this); + _numberTokenReader = TokenReader.ForNumbers(this); + } + + public float ReadNumber(string prompt) => ReadNumbers(prompt, 1)[0]; + + public (float, float) Read2Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 2); + return (numbers[0], numbers[1]); + } + + public (float, float, float) Read3Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 3); + return (numbers[0], numbers[1], numbers[2]); + } + + public (float, float, float, float) Read4Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 4); + return (numbers[0], numbers[1], numbers[2], numbers[3]); + } + + public void ReadNumbers(string prompt, float[] values) + { + if (values.Length == 0) + { + throw new ArgumentException($"'{nameof(values)}' must have a non-zero length.", nameof(values)); + } + + var numbers = _numberTokenReader.ReadTokens(prompt, (uint)values.Length).Select(t => t.Number).ToArray(); + numbers.CopyTo(values.AsSpan()); + } + + private IReadOnlyList ReadNumbers(string prompt, uint quantity) => + (quantity > 0) + ? _numberTokenReader.ReadTokens(prompt, quantity).Select(t => t.Number).ToList() + : throw new ArgumentOutOfRangeException( + nameof(quantity), + $"'{nameof(quantity)}' must be greater than zero."); + + public void Write(string value) => _output.Write(value); + + public void WriteLine(string value) => _output.WriteLine(value); + + public string ReadString(string prompt) + { + return ReadStrings(prompt, 1)[0]; + } + + public (string, string) Read2Strings(string prompt) + { + var values = ReadStrings(prompt, 2); + return (values[0], values[1]); + } + + private IReadOnlyList ReadStrings(string prompt, uint quantityRequired) => + _stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList(); + + internal string ReadLine(string prompt) + { + Write(prompt + "? "); + return _input.ReadLine(); + } + } +} \ No newline at end of file From fc92500074cdea9424b912e60ca9984d65b18cb6 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 22:30:40 +1100 Subject: [PATCH 10/21] Add ConsoleIO implementation and sample program --- 00_Common/dotnet/Games.Common.Sample/Program.cs | 7 +++++++ 00_Common/dotnet/Games.Common/IO/ConsoleIO.cs | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 00_Common/dotnet/Games.Common.Sample/Program.cs create mode 100644 00_Common/dotnet/Games.Common/IO/ConsoleIO.cs diff --git a/00_Common/dotnet/Games.Common.Sample/Program.cs b/00_Common/dotnet/Games.Common.Sample/Program.cs new file mode 100644 index 00000000..6bd2a8c4 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Sample/Program.cs @@ -0,0 +1,7 @@ +using Games.Common.IO; + +var io = new ConsoleIO(); + +var name = io.ReadString("What's your name"); + +io.WriteLine($"Hello, {name}"); diff --git a/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs new file mode 100644 index 00000000..842eb01c --- /dev/null +++ b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs @@ -0,0 +1,16 @@ +using System; + +namespace Games.Common.IO +{ + /// + /// An implementation of with input begin read for STDIN and output being written to + /// STDOUT. + /// + public sealed class ConsoleIO : TextIO + { + public ConsoleIO() + : base(Console.In, Console.Out) + { + } + } +} \ No newline at end of file From 35f68dcf72ccd2c42108712f143495f0fbcac9d2 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 22:33:44 +1100 Subject: [PATCH 11/21] Change to file-scoped namespaces --- .../IO/TextIOTests/ReadMethodTests.cs | 291 +++++++++--------- .../Games.Common.Test/IO/TokenReaderTests.cs | 187 ++++++----- .../dotnet/Games.Common.Test/IO/TokenTests.cs | 69 ++--- .../Games.Common.Test/IO/TokenizerTests.cs | 51 ++- 00_Common/dotnet/Games.Common/IO/ConsoleIO.cs | 21 +- .../dotnet/Games.Common/IO/IReadWrite.cs | 115 ++++--- 00_Common/dotnet/Games.Common/IO/Strings.cs | 13 +- 00_Common/dotnet/Games.Common/IO/TextIO.cs | 163 +++++----- 00_Common/dotnet/Games.Common/IO/Token.cs | 89 +++--- .../dotnet/Games.Common/IO/TokenReader.cs | 115 ++++--- 00_Common/dotnet/Games.Common/IO/Tokenizer.cs | 181 ++++++----- 11 files changed, 642 insertions(+), 653 deletions(-) diff --git a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs index 3b5de46d..119bacd0 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/ReadMethodTests.cs @@ -13,154 +13,153 @@ using FourNumbers = System.ValueTuple; using static System.Environment; using static Games.Common.IO.Strings; -namespace Games.Common.IO.TextIOTests +namespace Games.Common.IO.TextIOTests; + +public class ReadMethodTests { - public class ReadMethodTests + [Theory] + [MemberData(nameof(ReadStringTestCases))] + [MemberData(nameof(Read2StringsTestCases))] + [MemberData(nameof(ReadNumberTestCases))] + [MemberData(nameof(Read2NumbersTestCases))] + [MemberData(nameof(Read3NumbersTestCases))] + [MemberData(nameof(Read4NumbersTestCases))] + [MemberData(nameof(ReadNumbersTestCases))] + public void ReadingValuesHasExpectedPromptsAndResults( + Func read, + string input, + string expectedOutput, + T expectedResult) { - [Theory] - [MemberData(nameof(ReadStringTestCases))] - [MemberData(nameof(Read2StringsTestCases))] - [MemberData(nameof(ReadNumberTestCases))] - [MemberData(nameof(Read2NumbersTestCases))] - [MemberData(nameof(Read3NumbersTestCases))] - [MemberData(nameof(Read4NumbersTestCases))] - [MemberData(nameof(ReadNumbersTestCases))] - public void ReadingValuesHasExpectedPromptsAndResults( - Func read, - string input, - string expectedOutput, - T expectedResult) - { - var inputReader = new StringReader(input + Environment.NewLine); - var outputWriter = new StringWriter(); - var io = new TextIO(inputReader, outputWriter); + var inputReader = new StringReader(input + Environment.NewLine); + var outputWriter = new StringWriter(); + var io = new TextIO(inputReader, outputWriter); - var result = read.Invoke(io); - var output = outputWriter.ToString(); + var result = read.Invoke(io); + var output = outputWriter.ToString(); - using var _ = new AssertionScope(); - output.Should().Be(expectedOutput); - result.Should().BeEquivalentTo(expectedResult); - } - - [Fact] - public void ReadNumbers_ArrayEmpty_ThrowsArgumentException() - { - var io = new TextIO(new StringReader(""), new StringWriter()); - - Action readNumbers = () => io.ReadNumbers("foo", Array.Empty()); - - readNumbers.Should().Throw() - .WithMessage("'values' must have a non-zero length.*") - .WithParameterName("values"); - } - - public static TheoryData, string, string, string> ReadStringTestCases() - { - static Func ReadString(string prompt) => io => io.ReadString(prompt); - - return new() - { - { ReadString("Name"), "", "Name? ", "" }, - { ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" } - }; - } - - public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() - { - static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); - - return new() - { - { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, - { - Read2Strings("Input please"), - $"{NewLine}x,y", - $"Input please? ?? {ExtraInput}{NewLine}", - ("", "x") - } - }; - } - - public static TheoryData, string, string, float> ReadNumberTestCases() - { - static Func ReadNumber(string prompt) => io => io.ReadNumber(prompt); - - return new() - { - { ReadNumber("Age"), $"{NewLine}42,", $"Age? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", 42 }, - { ReadNumber("Guess"), "3,4,5", $"Guess? {ExtraInput}{NewLine}", 3 } - }; - } - - public static TheoryData, string, string, TwoNumbers> Read2NumbersTestCases() - { - static Func Read2Numbers(string prompt) => io => io.Read2Numbers(prompt); - - return new() - { - { Read2Numbers("Point"), "3,4,5", $"Point? {ExtraInput}{NewLine}", (3, 4) }, - { - Read2Numbers("Foo"), - $"x,4,5{NewLine}4,5,x", - $"Foo? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", - (4, 5) - } - }; - } - - public static TheoryData, string, string, ThreeNumbers> Read3NumbersTestCases() - { - static Func Read3Numbers(string prompt) => io => io.Read3Numbers(prompt); - - return new() - { - { Read3Numbers("Point"), "3.2, 4.3, 5.4, 6.5", $"Point? {ExtraInput}{NewLine}", (3.2F, 4.3F, 5.4F) }, - { - Read3Numbers("Bar"), - $"x,4,5{NewLine}4,5,x{NewLine}6,7,8,y", - $"Bar? {NumberExpected}{NewLine}? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", - (6, 7, 8) - } - }; - } - - public static TheoryData, string, string, FourNumbers> Read4NumbersTestCases() - { - static Func Read4Numbers(string prompt) => io => io.Read4Numbers(prompt); - - return new() - { - { Read4Numbers("Point"), "3,4,5,6,7", $"Point? {ExtraInput}{NewLine}", (3, 4, 5, 6) }, - { - Read4Numbers("Baz"), - $"x,4,5,6{NewLine} 4, 5 , 6,7 ,x", - $"Baz? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", - (4, 5, 6, 7) - } - }; - } - - public static TheoryData>, string, string, float[]> ReadNumbersTestCases() - { - static Func> ReadNumbers(string prompt) => - io => - { - var numbers = new float[6]; - io.ReadNumbers(prompt, numbers); - return numbers; - }; - - return new() - { - { ReadNumbers("Primes"), "2, 3, 5, 7, 11, 13", $"Primes? ", new float[] { 2, 3, 5, 7, 11, 13 } }, - { - ReadNumbers("Qux"), - $"42{NewLine}3.141, 2.718{NewLine}3.0e8, 6.02e23{NewLine}9.11E-28", - $"Qux? ?? ?? ?? ", - new[] { 42, 3.141F, 2.718F, 3.0e8F, 6.02e23F, 9.11E-28F } - } - }; - } + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Should().BeEquivalentTo(expectedResult); } -} \ No newline at end of file + + [Fact] + public void ReadNumbers_ArrayEmpty_ThrowsArgumentException() + { + var io = new TextIO(new StringReader(""), new StringWriter()); + + Action readNumbers = () => io.ReadNumbers("foo", Array.Empty()); + + readNumbers.Should().Throw() + .WithMessage("'values' must have a non-zero length.*") + .WithParameterName("values"); + } + + public static TheoryData, string, string, string> ReadStringTestCases() + { + static Func ReadString(string prompt) => io => io.ReadString(prompt); + + return new() + { + { ReadString("Name"), "", "Name? ", "" }, + { ReadString("prompt"), " foo ,bar", $"prompt? {ExtraInput}{NewLine}", "foo" } + }; + } + + public static TheoryData, string, string, TwoStrings> Read2StringsTestCases() + { + static Func Read2Strings(string prompt) => io => io.Read2Strings(prompt); + + return new() + { + { Read2Strings("2 strings"), ",", "2 strings? ", ("", "") }, + { + Read2Strings("Input please"), + $"{NewLine}x,y", + $"Input please? ?? {ExtraInput}{NewLine}", + ("", "x") + } + }; + } + + public static TheoryData, string, string, float> ReadNumberTestCases() + { + static Func ReadNumber(string prompt) => io => io.ReadNumber(prompt); + + return new() + { + { ReadNumber("Age"), $"{NewLine}42,", $"Age? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", 42 }, + { ReadNumber("Guess"), "3,4,5", $"Guess? {ExtraInput}{NewLine}", 3 } + }; + } + + public static TheoryData, string, string, TwoNumbers> Read2NumbersTestCases() + { + static Func Read2Numbers(string prompt) => io => io.Read2Numbers(prompt); + + return new() + { + { Read2Numbers("Point"), "3,4,5", $"Point? {ExtraInput}{NewLine}", (3, 4) }, + { + Read2Numbers("Foo"), + $"x,4,5{NewLine}4,5,x", + $"Foo? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5) + } + }; + } + + public static TheoryData, string, string, ThreeNumbers> Read3NumbersTestCases() + { + static Func Read3Numbers(string prompt) => io => io.Read3Numbers(prompt); + + return new() + { + { Read3Numbers("Point"), "3.2, 4.3, 5.4, 6.5", $"Point? {ExtraInput}{NewLine}", (3.2F, 4.3F, 5.4F) }, + { + Read3Numbers("Bar"), + $"x,4,5{NewLine}4,5,x{NewLine}6,7,8,y", + $"Bar? {NumberExpected}{NewLine}? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (6, 7, 8) + } + }; + } + + public static TheoryData, string, string, FourNumbers> Read4NumbersTestCases() + { + static Func Read4Numbers(string prompt) => io => io.Read4Numbers(prompt); + + return new() + { + { Read4Numbers("Point"), "3,4,5,6,7", $"Point? {ExtraInput}{NewLine}", (3, 4, 5, 6) }, + { + Read4Numbers("Baz"), + $"x,4,5,6{NewLine} 4, 5 , 6,7 ,x", + $"Baz? {NumberExpected}{NewLine}? {ExtraInput}{NewLine}", + (4, 5, 6, 7) + } + }; + } + + public static TheoryData>, string, string, float[]> ReadNumbersTestCases() + { + static Func> ReadNumbers(string prompt) => + io => + { + var numbers = new float[6]; + io.ReadNumbers(prompt, numbers); + return numbers; + }; + + return new() + { + { ReadNumbers("Primes"), "2, 3, 5, 7, 11, 13", $"Primes? ", new float[] { 2, 3, 5, 7, 11, 13 } }, + { + ReadNumbers("Qux"), + $"42{NewLine}3.141, 2.718{NewLine}3.0e8, 6.02e23{NewLine}9.11E-28", + $"Qux? ?? ?? ?? ", + new[] { 42, 3.141F, 2.718F, 3.0e8F, 6.02e23F, 9.11E-28F } + } + }; + } +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs index 65b33908..d4d8458e 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenReaderTests.cs @@ -8,100 +8,99 @@ using Xunit; using static System.Environment; using static Games.Common.IO.Strings; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenReaderTests { - public class TokenReaderTests + private readonly StringWriter _outputWriter; + + public TokenReaderTests() { - private readonly StringWriter _outputWriter; - - public TokenReaderTests() - { - _outputWriter = new StringWriter(); - } - - [Fact] - public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() - { - var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); - - Action readTokens = () => sut.ReadTokens("", 0); - - readTokens.Should().Throw() - .WithMessage("'quantityNeeded' must be greater than zero.*") - .WithParameterName("quantityNeeded"); - } - - - [Theory] - [MemberData(nameof(ReadTokensTestCases))] - public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( - string prompt, - uint tokenCount, - string input, - string expectedOutput, - string[] expectedResult) - { - var sut = TokenReader.ForStrings(new TextIO(new StringReader(input + NewLine), _outputWriter)); - - var result = sut.ReadTokens(prompt, tokenCount); - var output = _outputWriter.ToString(); - - using var _ = new AssertionScope(); - output.Should().Be(expectedOutput); - result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); - } - - [Theory] - [MemberData(nameof(ReadNumericTokensTestCases))] - public void ReadTokens_Numeric_ReadingValuesHasExpectedPromptsAndResults( - string prompt, - uint tokenCount, - string input, - string expectedOutput, - float[] expectedResult) - { - var sut = TokenReader.ForNumbers(new TextIO(new StringReader(input + NewLine), _outputWriter)); - - var result = sut.ReadTokens(prompt, tokenCount); - var output = _outputWriter.ToString(); - - using var _ = new AssertionScope(); - output.Should().Be(expectedOutput); - result.Select(t => t.Number).Should().BeEquivalentTo(expectedResult); - } - - public static TheoryData ReadTokensTestCases() - { - return new() - { - { "Name", 1, "Bill", "Name? ", new[] { "Bill" } }, - { "Names", 2, " Bill , Bloggs ", "Names? ", new[] { "Bill", "Bloggs" } }, - { "Names", 2, $" Bill{NewLine}Bloggs ", "Names? ?? ", new[] { "Bill", "Bloggs" } }, - { - "Foo", - 6, - $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", - $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", - new[] { "1", "2", " a,b ", "", "", "d\"x" } - } - }; - } - - public static TheoryData ReadNumericTokensTestCases() - { - return new() - { - { "Age", 1, "23", "Age? ", new[] { 23F } }, - { "Constants", 2, " 3.141 , 2.71 ", "Constants? ", new[] { 3.141F, 2.71F } }, - { "Answer", 1, $"Forty-two{NewLine}42 ", $"Answer? {NumberExpected}{NewLine}? ", new[] { 42F } }, - { - "Foo", - 6, - $"1,2{NewLine}\" a,b \"{NewLine}3, 4 {NewLine}5.6,7,a, b", - $"Foo? ?? {NumberExpected}{NewLine}? ?? {ExtraInput}{NewLine}", - new[] { 1, 2, 3, 4, 5.6F, 7 } - } - }; - } + _outputWriter = new StringWriter(); } -} \ No newline at end of file + + [Fact] + public void ReadTokens_QuantityNeededZero_ThrowsArgumentException() + { + var sut = TokenReader.ForStrings(new TextIO(new StringReader(""), _outputWriter)); + + Action readTokens = () => sut.ReadTokens("", 0); + + readTokens.Should().Throw() + .WithMessage("'quantityNeeded' must be greater than zero.*") + .WithParameterName("quantityNeeded"); + } + + + [Theory] + [MemberData(nameof(ReadTokensTestCases))] + public void ReadTokens_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + string[] expectedResult) + { + var sut = TokenReader.ForStrings(new TextIO(new StringReader(input + NewLine), _outputWriter)); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.String).Should().BeEquivalentTo(expectedResult); + } + + [Theory] + [MemberData(nameof(ReadNumericTokensTestCases))] + public void ReadTokens_Numeric_ReadingValuesHasExpectedPromptsAndResults( + string prompt, + uint tokenCount, + string input, + string expectedOutput, + float[] expectedResult) + { + var sut = TokenReader.ForNumbers(new TextIO(new StringReader(input + NewLine), _outputWriter)); + + var result = sut.ReadTokens(prompt, tokenCount); + var output = _outputWriter.ToString(); + + using var _ = new AssertionScope(); + output.Should().Be(expectedOutput); + result.Select(t => t.Number).Should().BeEquivalentTo(expectedResult); + } + + public static TheoryData ReadTokensTestCases() + { + return new() + { + { "Name", 1, "Bill", "Name? ", new[] { "Bill" } }, + { "Names", 2, " Bill , Bloggs ", "Names? ", new[] { "Bill", "Bloggs" } }, + { "Names", 2, $" Bill{NewLine}Bloggs ", "Names? ?? ", new[] { "Bill", "Bloggs" } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine},\"\"c,d{NewLine}d\"x,e,f", + $"Foo? ?? ?? ?? {ExtraInput}{NewLine}", + new[] { "1", "2", " a,b ", "", "", "d\"x" } + } + }; + } + + public static TheoryData ReadNumericTokensTestCases() + { + return new() + { + { "Age", 1, "23", "Age? ", new[] { 23F } }, + { "Constants", 2, " 3.141 , 2.71 ", "Constants? ", new[] { 3.141F, 2.71F } }, + { "Answer", 1, $"Forty-two{NewLine}42 ", $"Answer? {NumberExpected}{NewLine}? ", new[] { 42F } }, + { + "Foo", + 6, + $"1,2{NewLine}\" a,b \"{NewLine}3, 4 {NewLine}5.6,7,a, b", + $"Foo? ?? {NumberExpected}{NewLine}? ?? {ExtraInput}{NewLine}", + new[] { 1, 2, 3, 4, 5.6F, 7 } + } + }; + } +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs index f716ca78..e91922d8 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenTests.cs @@ -1,43 +1,42 @@ using FluentAssertions; using Xunit; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenTests { - public class TokenTests + [Theory] + [MemberData(nameof(TokenTestCases))] + public void Ctor_PopulatesProperties(string value, bool isNumber, float number) { - [Theory] - [MemberData(nameof(TokenTestCases))] - public void Ctor_PopulatesProperties(string value, bool isNumber, float number) - { - var expected = new { String = value, IsNumber = isNumber, Number = number }; + var expected = new { String = value, IsNumber = isNumber, Number = number }; - var token = new Token(value); + var token = new Token(value); - token.Should().BeEquivalentTo(expected); - } - - public static TheoryData TokenTestCases() => new() - { - { "", false, float.NaN }, - { "abcde", false, float.NaN }, - { "123 ", true, 123 }, - { "+42 ", true, 42 }, - { "-42 ", true, -42 }, - { "+3.14159 ", true, 3.14159F }, - { "-3.14159 ", true, -3.14159F }, - { " 123", false, float.NaN }, - { "1.2e4", true, 12000 }, - { "2.3e-5", true, 0.000023F }, - { "1e100", true, float.MaxValue }, - { "-1E100", true, float.MinValue }, - { "1E-100", true, 0 }, - { "-1e-100", true, 0 }, - { "100abc", true, 100 }, - { "1,2,3", true, 1 }, - { "42,a,b", true, 42 }, - { "1.2.3", true, 1.2F }, - { "12e.5", false, float.NaN }, - { "12e0.5", true, 12 } - }; + token.Should().BeEquivalentTo(expected); } -} \ No newline at end of file + + public static TheoryData TokenTestCases() => new() + { + { "", false, float.NaN }, + { "abcde", false, float.NaN }, + { "123 ", true, 123 }, + { "+42 ", true, 42 }, + { "-42 ", true, -42 }, + { "+3.14159 ", true, 3.14159F }, + { "-3.14159 ", true, -3.14159F }, + { " 123", false, float.NaN }, + { "1.2e4", true, 12000 }, + { "2.3e-5", true, 0.000023F }, + { "1e100", true, float.MaxValue }, + { "-1E100", true, float.MinValue }, + { "1E-100", true, 0 }, + { "-1e-100", true, 0 }, + { "100abc", true, 100 }, + { "1,2,3", true, 1 }, + { "42,a,b", true, 42 }, + { "1.2.3", true, 1.2F }, + { "12e.5", false, float.NaN }, + { "12e0.5", true, 12 } + }; +} diff --git a/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs index a4e2f762..30e54b0f 100644 --- a/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs +++ b/00_Common/dotnet/Games.Common.Test/IO/TokenizerTests.cs @@ -2,33 +2,32 @@ using System.Linq; using FluentAssertions; using Xunit; -namespace Games.Common.IO +namespace Games.Common.IO; + +public class TokenizerTests { - public class TokenizerTests + [Theory] + [MemberData(nameof(TokenizerTestCases))] + public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected) { - [Theory] - [MemberData(nameof(TokenizerTestCases))] - public void ParseTokens_SplitsStringIntoExpectedTokens(string input, string[] expected) - { - var result = Tokenizer.ParseTokens(input); + var result = Tokenizer.ParseTokens(input); - result.Select(t => t.ToString()).Should().BeEquivalentTo(expected); - } - - public static TheoryData TokenizerTestCases() => new() - { - { "", new[] { "" } }, - { "aBc", new[] { "aBc" } }, - { " Foo ", new[] { "Foo" } }, - { " \" Foo \" ", new[] { " Foo " } }, - { " \" Foo ", new[] { " Foo " } }, - { "\"\"abc", new[] { "" } }, - { "a\"\"bc", new[] { "a\"\"bc" } }, - { "\"\"", new[] { "" } }, - { ",", new[] { "", "" } }, - { " foo ,bar", new[] { "foo", "bar" } }, - { "\"a\"bc,de", new[] { "a" } }, - { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } - }; + result.Select(t => t.ToString()).Should().BeEquivalentTo(expected); } -} \ No newline at end of file + + public static TheoryData TokenizerTestCases() => new() + { + { "", new[] { "" } }, + { "aBc", new[] { "aBc" } }, + { " Foo ", new[] { "Foo" } }, + { " \" Foo \" ", new[] { " Foo " } }, + { " \" Foo ", new[] { " Foo " } }, + { "\"\"abc", new[] { "" } }, + { "a\"\"bc", new[] { "a\"\"bc" } }, + { "\"\"", new[] { "" } }, + { ",", new[] { "", "" } }, + { " foo ,bar", new[] { "foo", "bar" } }, + { "\"a\"bc,de", new[] { "a" } }, + { "a\"b,\" c,d\", f ,,g", new[] { "a\"b", " c,d", "f", "", "g" } } + }; +} diff --git a/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs index 842eb01c..a24238bf 100644 --- a/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs +++ b/00_Common/dotnet/Games.Common/IO/ConsoleIO.cs @@ -1,16 +1,15 @@ using System; -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// An implementation of with input begin read for STDIN and output being written to +/// STDOUT. +/// +public sealed class ConsoleIO : TextIO { - /// - /// An implementation of with input begin read for STDIN and output being written to - /// STDOUT. - /// - public sealed class ConsoleIO : TextIO + public ConsoleIO() + : base(Console.In, Console.Out) { - public ConsoleIO() - : base(Console.In, Console.Out) - { - } } -} \ No newline at end of file +} diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs index c5d24101..629b22aa 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -1,69 +1,68 @@ -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// Provides for input and output of strings and numbers. +/// +public interface IReadWrite { /// - /// Provides for input and output of strings and numbers. + /// Reads a value from input. /// - public interface IReadWrite - { - /// - /// Reads a value from input. - /// - /// The text to display to prompt for the value. - /// A , being the value entered. - float ReadNumber(string prompt); + /// The text to display to prompt for the value. + /// A , being the value entered. + float ReadNumber(string prompt); - /// - /// Reads 2 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float) Read2Numbers(string prompt); + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float) Read2Numbers(string prompt); - /// - /// Reads 3 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float, float) Read3Numbers(string prompt); + /// + /// Reads 3 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float) Read3Numbers(string prompt); - /// - /// Reads 4 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (float, float, float, float) Read4Numbers(string prompt); + /// + /// Reads 4 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (float, float, float, float) Read4Numbers(string prompt); - /// - /// Read numbers from input to fill an array. - /// - /// The text to display to prompt for the values. - /// A to be filled with values from input. - void ReadNumbers(string prompt, float[] values); + /// + /// Read numbers from input to fill an array. + /// + /// The text to display to prompt for the values. + /// A to be filled with values from input. + void ReadNumbers(string prompt, float[] values); - /// - /// Reads a value from input. - /// - /// The text to display to prompt for the value. - /// A , being the value entered. - string ReadString(string prompt); + /// + /// Reads a value from input. + /// + /// The text to display to prompt for the value. + /// A , being the value entered. + string ReadString(string prompt); - /// - /// Reads 2 values from input. - /// - /// The text to display to prompt for the values. - /// A , being the values entered. - (string, string) Read2Strings(string prompt); + /// + /// Reads 2 values from input. + /// + /// The text to display to prompt for the values. + /// A , being the values entered. + (string, string) Read2Strings(string prompt); - /// - /// Writes a to output. - /// - /// The to be written. - void Write(string message); + /// + /// Writes a to output. + /// + /// The to be written. + void Write(string message); - /// - /// Writes a to output, followed by a new-line. - /// - /// The to be written. - void WriteLine(string message); - } -} \ No newline at end of file + /// + /// Writes a to output, followed by a new-line. + /// + /// The to be written. + void WriteLine(string message); +} diff --git a/00_Common/dotnet/Games.Common/IO/Strings.cs b/00_Common/dotnet/Games.Common/IO/Strings.cs index 3d955409..cf7d0f43 100644 --- a/00_Common/dotnet/Games.Common/IO/Strings.cs +++ b/00_Common/dotnet/Games.Common/IO/Strings.cs @@ -1,8 +1,7 @@ -namespace Games.Common.IO +namespace Games.Common.IO; + +internal static class Strings { - internal static class Strings - { - internal const string NumberExpected = "!Number expected - retry input line"; - internal const string ExtraInput = "!Extra input ignored"; - } -} \ No newline at end of file + internal const string NumberExpected = "!Number expected - retry input line"; + internal const string ExtraInput = "!Extra input ignored"; +} diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs index e210ec4e..d4d80e54 100644 --- a/00_Common/dotnet/Games.Common/IO/TextIO.cs +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -3,88 +3,87 @@ using System.Collections.Generic; using System.IO; using System.Linq; -namespace Games.Common.IO +namespace Games.Common.IO; + +/// +/// +/// Implements with input read from a and output written to a +/// . +/// +public class TextIO : IReadWrite { - /// - /// - /// Implements with input read from a and output written to a - /// . - /// - public class TextIO : IReadWrite + private readonly TextReader _input; + private readonly TextWriter _output; + private readonly TokenReader _stringTokenReader; + private readonly TokenReader _numberTokenReader; + + public TextIO(TextReader input, TextWriter output) { - private readonly TextReader _input; - private readonly TextWriter _output; - private readonly TokenReader _stringTokenReader; - private readonly TokenReader _numberTokenReader; - - public TextIO(TextReader input, TextWriter output) - { - _input = input ?? throw new ArgumentNullException(nameof(input)); - _output = output ?? throw new ArgumentNullException(nameof(output)); - _stringTokenReader = TokenReader.ForStrings(this); - _numberTokenReader = TokenReader.ForNumbers(this); - } - - public float ReadNumber(string prompt) => ReadNumbers(prompt, 1)[0]; - - public (float, float) Read2Numbers(string prompt) - { - var numbers = ReadNumbers(prompt, 2); - return (numbers[0], numbers[1]); - } - - public (float, float, float) Read3Numbers(string prompt) - { - var numbers = ReadNumbers(prompt, 3); - return (numbers[0], numbers[1], numbers[2]); - } - - public (float, float, float, float) Read4Numbers(string prompt) - { - var numbers = ReadNumbers(prompt, 4); - return (numbers[0], numbers[1], numbers[2], numbers[3]); - } - - public void ReadNumbers(string prompt, float[] values) - { - if (values.Length == 0) - { - throw new ArgumentException($"'{nameof(values)}' must have a non-zero length.", nameof(values)); - } - - var numbers = _numberTokenReader.ReadTokens(prompt, (uint)values.Length).Select(t => t.Number).ToArray(); - numbers.CopyTo(values.AsSpan()); - } - - private IReadOnlyList ReadNumbers(string prompt, uint quantity) => - (quantity > 0) - ? _numberTokenReader.ReadTokens(prompt, quantity).Select(t => t.Number).ToList() - : throw new ArgumentOutOfRangeException( - nameof(quantity), - $"'{nameof(quantity)}' must be greater than zero."); - - public void Write(string value) => _output.Write(value); - - public void WriteLine(string value) => _output.WriteLine(value); - - public string ReadString(string prompt) - { - return ReadStrings(prompt, 1)[0]; - } - - public (string, string) Read2Strings(string prompt) - { - var values = ReadStrings(prompt, 2); - return (values[0], values[1]); - } - - private IReadOnlyList ReadStrings(string prompt, uint quantityRequired) => - _stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList(); - - internal string ReadLine(string prompt) - { - Write(prompt + "? "); - return _input.ReadLine(); - } + _input = input ?? throw new ArgumentNullException(nameof(input)); + _output = output ?? throw new ArgumentNullException(nameof(output)); + _stringTokenReader = TokenReader.ForStrings(this); + _numberTokenReader = TokenReader.ForNumbers(this); } -} \ No newline at end of file + + public float ReadNumber(string prompt) => ReadNumbers(prompt, 1)[0]; + + public (float, float) Read2Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 2); + return (numbers[0], numbers[1]); + } + + public (float, float, float) Read3Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 3); + return (numbers[0], numbers[1], numbers[2]); + } + + public (float, float, float, float) Read4Numbers(string prompt) + { + var numbers = ReadNumbers(prompt, 4); + return (numbers[0], numbers[1], numbers[2], numbers[3]); + } + + public void ReadNumbers(string prompt, float[] values) + { + if (values.Length == 0) + { + throw new ArgumentException($"'{nameof(values)}' must have a non-zero length.", nameof(values)); + } + + var numbers = _numberTokenReader.ReadTokens(prompt, (uint)values.Length).Select(t => t.Number).ToArray(); + numbers.CopyTo(values.AsSpan()); + } + + private IReadOnlyList ReadNumbers(string prompt, uint quantity) => + (quantity > 0) + ? _numberTokenReader.ReadTokens(prompt, quantity).Select(t => t.Number).ToList() + : throw new ArgumentOutOfRangeException( + nameof(quantity), + $"'{nameof(quantity)}' must be greater than zero."); + + public void Write(string value) => _output.Write(value); + + public void WriteLine(string value) => _output.WriteLine(value); + + public string ReadString(string prompt) + { + return ReadStrings(prompt, 1)[0]; + } + + public (string, string) Read2Strings(string prompt) + { + var values = ReadStrings(prompt, 2); + return (values[0], values[1]); + } + + private IReadOnlyList ReadStrings(string prompt, uint quantityRequired) => + _stringTokenReader.ReadTokens(prompt, quantityRequired).Select(t => t.String).ToList(); + + internal string ReadLine(string prompt) + { + Write(prompt + "? "); + return _input.ReadLine(); + } +} diff --git a/00_Common/dotnet/Games.Common/IO/Token.cs b/00_Common/dotnet/Games.Common/IO/Token.cs index f1520c8e..dc432e0c 100644 --- a/00_Common/dotnet/Games.Common/IO/Token.cs +++ b/00_Common/dotnet/Games.Common/IO/Token.cs @@ -1,60 +1,59 @@ using System.Text; using System.Text.RegularExpressions; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class Token { - internal class Token + private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); + + internal Token(string value) { - private static readonly Regex _numberPattern = new(@"^[+\-]?\d*(\.\d*)?([eE][+\-]?\d*)?"); + String = value; - internal Token(string value) + var match = _numberPattern.Match(String); + + IsNumber = float.TryParse(match.Value, out var number); + Number = (IsNumber, number) switch { - String = value; + (false, _) => float.NaN, + (true, float.PositiveInfinity) => float.MaxValue, + (true, float.NegativeInfinity) => float.MinValue, + (true, _) => number + }; + } - var match = _numberPattern.Match(String); + public string String { get; } + public bool IsNumber { get; } + public float Number { get; } - IsNumber = float.TryParse(match.Value, out var number); - Number = (IsNumber, number) switch - { - (false, _) => float.NaN, - (true, float.PositiveInfinity) => float.MaxValue, - (true, float.NegativeInfinity) => float.MinValue, - (true, _) => number - }; + public override string ToString() => String; + + internal class Builder + { + private readonly StringBuilder _builder = new(); + private bool _isQuoted; + private int _trailingWhiteSpaceCount; + + public Builder Append(char character) + { + _builder.Append(character); + + _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; + + return this; } - public string String { get; } - public bool IsNumber { get; } - public float Number { get; } - - public override string ToString() => String; - - internal class Builder + public Builder SetIsQuoted() { - private readonly StringBuilder _builder = new(); - private bool _isQuoted; - private int _trailingWhiteSpaceCount; + _isQuoted = true; + return this; + } - public Builder Append(char character) - { - _builder.Append(character); - - _trailingWhiteSpaceCount = char.IsWhiteSpace(character) ? _trailingWhiteSpaceCount + 1 : 0; - - return this; - } - - public Builder SetIsQuoted() - { - _isQuoted = true; - return this; - } - - public Token Build() - { - if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; } - return new Token(_builder.ToString()); - } + public Token Build() + { + if (!_isQuoted) { _builder.Length -= _trailingWhiteSpaceCount; } + return new Token(_builder.ToString()); } } -} \ No newline at end of file +} diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index b7eb3774..3219c459 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -3,79 +3,78 @@ using System.Collections.Generic; using static Games.Common.IO.Strings; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class TokenReader { - internal class TokenReader + private readonly TextIO _io; + private readonly Predicate _isTokenValid; + + private TokenReader(TextIO io, Predicate isTokenValid) { - private readonly TextIO _io; - private readonly Predicate _isTokenValid; + _io = io; + _isTokenValid = isTokenValid ?? (t => true); + } - private TokenReader(TextIO io, Predicate isTokenValid) + public static TokenReader ForStrings(TextIO io) => new(io, t => true); + public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + + public IEnumerable ReadTokens(string prompt, uint quantityNeeded) + { + if (quantityNeeded == 0) { - _io = io; - _isTokenValid = isTokenValid ?? (t => true); + throw new ArgumentOutOfRangeException( + nameof(quantityNeeded), + $"'{nameof(quantityNeeded)}' must be greater than zero."); } - public static TokenReader ForStrings(TextIO io) => new(io, t => true); - public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + var tokens = new List(); - public IEnumerable ReadTokens(string prompt, uint quantityNeeded) + while (tokens.Count < quantityNeeded) { - if (quantityNeeded == 0) - { - throw new ArgumentOutOfRangeException( - nameof(quantityNeeded), - $"'{nameof(quantityNeeded)}' must be greater than zero."); - } + tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count)); + prompt = "?"; + } + return tokens; + } + + private IEnumerable ReadValidTokens(string prompt, uint maxCount) + { + while (true) + { + var tokensValid = true; var tokens = new List(); - - while(tokens.Count < quantityNeeded) + foreach (var token in ReadLineOfTokens(prompt, maxCount)) { - tokens.AddRange(ReadValidTokens(prompt, quantityNeeded - (uint)tokens.Count)); - prompt = "?"; - } - - return tokens; - } - - private IEnumerable ReadValidTokens(string prompt, uint maxCount) - { - while (true) - { - var tokensValid = true; - var tokens = new List(); - foreach (var token in ReadLineOfTokens(prompt, maxCount)) + if (!_isTokenValid(token)) { - if (!_isTokenValid(token)) - { - _io.WriteLine(NumberExpected); - tokensValid = false; - prompt = ""; - break; - } - - tokens.Add(token); - } - - if (tokensValid) { return tokens; } - } - } - - private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) - { - var tokenCount = 0; - - foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt))) - { - if (++tokenCount > maxCount) - { - _io.WriteLine(ExtraInput); + _io.WriteLine(NumberExpected); + tokensValid = false; + prompt = ""; break; } - yield return token; + tokens.Add(token); } + + if (tokensValid) { return tokens; } } } -} \ No newline at end of file + + private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) + { + var tokenCount = 0; + + foreach (var token in Tokenizer.ParseTokens(_io.ReadLine(prompt))) + { + if (++tokenCount > maxCount) + { + _io.WriteLine(ExtraInput); + break; + } + + yield return token; + } + } +} diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs index 857b3331..fdaf829e 100644 --- a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -1,102 +1,101 @@ using System; using System.Collections.Generic; -namespace Games.Common.IO +namespace Games.Common.IO; + +internal class Tokenizer { - internal class Tokenizer + private const char Quote = '"'; + private const char Separator = ','; + + private readonly Queue _characters; + + private Tokenizer(string input) => _characters = new Queue(input); + + public static IEnumerable ParseTokens(string input) { - private const char Quote = '"'; - private const char Separator = ','; + if (input is null) { throw new ArgumentNullException(nameof(input)); } - private readonly Queue _characters; + return new Tokenizer(input).ParseTokens(); + } - private Tokenizer(string input) => _characters = new Queue(input); - - public static IEnumerable ParseTokens(string input) + private IEnumerable ParseTokens() + { + while (true) { - if (input is null) { throw new ArgumentNullException(nameof(input)); } + var (token, isLastToken) = Consume(_characters); + yield return token; - return new Tokenizer(input).ParseTokens(); - } - - private IEnumerable ParseTokens() - { - while (true) - { - var (token, isLastToken) = Consume(_characters); - yield return token; - - if (isLastToken) { break; } - } - } - - public (Token, bool) Consume(Queue characters) - { - var tokenBuilder = new Token.Builder(); - var state = ITokenizerState.LookForStartOfToken; - - while (characters.TryDequeue(out var character)) - { - (state, tokenBuilder) = state.Consume(character, tokenBuilder); - if (state is AtEndOfTokenState) { return (tokenBuilder.Build(), false); } - } - - return (tokenBuilder.Build(), true); - } - - private interface ITokenizerState - { - public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); - - (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder); - } - - private struct LookForStartOfTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character switch - { - Separator => (new AtEndOfTokenState(), tokenBuilder), - Quote => (new InQuotedTokenState(), tokenBuilder.SetIsQuoted()), - _ when char.IsWhiteSpace(character) => (this, tokenBuilder), - _ => (new InTokenState(), tokenBuilder.Append(character)) - }; - } - - private struct InTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Separator - ? (new AtEndOfTokenState(), tokenBuilder) - : (this, tokenBuilder.Append(character)); - } - - private struct InQuotedTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Quote - ? (new ExpectSeparatorState(), tokenBuilder) - : (this, tokenBuilder.Append(character)); - } - - private struct ExpectSeparatorState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - character == Separator - ? (new AtEndOfTokenState(), tokenBuilder) - : (new IgnoreRestOfLineState(), tokenBuilder); - } - - private struct IgnoreRestOfLineState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - (this, tokenBuilder); - } - - private struct AtEndOfTokenState : ITokenizerState - { - public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => - throw new InvalidOperationException(); + if (isLastToken) { break; } } } -} \ No newline at end of file + + public (Token, bool) Consume(Queue characters) + { + var tokenBuilder = new Token.Builder(); + var state = ITokenizerState.LookForStartOfToken; + + while (characters.TryDequeue(out var character)) + { + (state, tokenBuilder) = state.Consume(character, tokenBuilder); + if (state is AtEndOfTokenState) { return (tokenBuilder.Build(), false); } + } + + return (tokenBuilder.Build(), true); + } + + private interface ITokenizerState + { + public static ITokenizerState LookForStartOfToken { get; } = new LookForStartOfTokenState(); + + (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder); + } + + private struct LookForStartOfTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character switch + { + Separator => (new AtEndOfTokenState(), tokenBuilder), + Quote => (new InQuotedTokenState(), tokenBuilder.SetIsQuoted()), + _ when char.IsWhiteSpace(character) => (this, tokenBuilder), + _ => (new InTokenState(), tokenBuilder.Append(character)) + }; + } + + private struct InTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); + } + + private struct InQuotedTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Quote + ? (new ExpectSeparatorState(), tokenBuilder) + : (this, tokenBuilder.Append(character)); + } + + private struct ExpectSeparatorState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + character == Separator + ? (new AtEndOfTokenState(), tokenBuilder) + : (new IgnoreRestOfLineState(), tokenBuilder); + } + + private struct IgnoreRestOfLineState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + (this, tokenBuilder); + } + + private struct AtEndOfTokenState : ITokenizerState + { + public (ITokenizerState, Token.Builder) Consume(char character, Token.Builder tokenBuilder) => + throw new InvalidOperationException(); + } +} From 728c5b8a00cb5e111363432e45713d56bae0c307 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 15 Feb 2022 22:37:09 +1100 Subject: [PATCH 12/21] Add remarks comment --- 00_Common/dotnet/Games.Common/IO/TextIO.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs index d4d80e54..5a7cf753 100644 --- a/00_Common/dotnet/Games.Common/IO/TextIO.cs +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -10,6 +10,10 @@ namespace Games.Common.IO; /// Implements with input read from a and output written to a /// . /// +/// +/// This implementation reproduces the Vintage BASIC input experience, prompting multiple times when partial input +/// supplied, rejecting non-numeric input as needed, warning about extra input being ignored, etc. +/// public class TextIO : IReadWrite { private readonly TextReader _input; From 8d13695e72ef0f57a3dbd27dba1bf10c1ded5aae Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 16 Feb 2022 22:47:35 +1100 Subject: [PATCH 13/21] Add more comments --- .../dotnet/Games.Common/IO/TokenReader.cs | 34 ++++++++++++++++++- 00_Common/dotnet/Games.Common/IO/Tokenizer.cs | 3 ++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/00_Common/dotnet/Games.Common/IO/TokenReader.cs b/00_Common/dotnet/Games.Common/IO/TokenReader.cs index 3219c459..04e7ad41 100644 --- a/00_Common/dotnet/Games.Common/IO/TokenReader.cs +++ b/00_Common/dotnet/Games.Common/IO/TokenReader.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; - +using System.Linq; using static Games.Common.IO.Strings; namespace Games.Common.IO; +/// +/// Reads from input and assembles a given number of values, or tokens, possibly over a number of input lines. +/// internal class TokenReader { private readonly TextIO _io; @@ -16,9 +19,26 @@ internal class TokenReader _isTokenValid = isTokenValid ?? (t => true); } + /// + /// Creates a which reads string tokens. + /// + /// A instance. + /// The new instance. public static TokenReader ForStrings(TextIO io) => new(io, t => true); + + /// + /// Creates a which reads tokens and validates that they can be parsed as numbers. + /// + /// A instance. + /// The new instance. public static TokenReader ForNumbers(TextIO io) => new(io, t => t.IsNumber); + /// + /// Reads valid tokens from one or more input lines and builds a list with the required quantity. + /// + /// The string used to prompt the user for input. + /// The number of tokens required. + /// The sequence of tokens read. public IEnumerable ReadTokens(string prompt, uint quantityNeeded) { if (quantityNeeded == 0) @@ -39,6 +59,12 @@ internal class TokenReader return tokens; } + /// + /// Reads a line of tokens, up to , and rejects the line if any are invalid. + /// + /// The string used to prompt the user for input. + /// The maximum number of tokens to read. + /// The sequence of tokens read. private IEnumerable ReadValidTokens(string prompt, uint maxCount) { while (true) @@ -62,6 +88,12 @@ internal class TokenReader } } + /// + /// Lazily reads up to tokens from an input line. + /// + /// The string used to prompt the user for input. + /// The maximum number of tokens to read. + /// private IEnumerable ReadLineOfTokens(string prompt, uint maxCount) { var tokenCount = 0; diff --git a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs index fdaf829e..dc6a4d79 100644 --- a/00_Common/dotnet/Games.Common/IO/Tokenizer.cs +++ b/00_Common/dotnet/Games.Common/IO/Tokenizer.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; namespace Games.Common.IO; +/// +/// A simple state machine which parses tokens from a line of input. +/// internal class Tokenizer { private const char Quote = '"'; From 08f0ab0dc46dabfaf6f7a12a61ea3d40090e1766 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Tue, 1 Mar 2022 07:39:16 +1100 Subject: [PATCH 14/21] Format numbers in output --- .../IO/TextIOTests/NumberFormatTests.cs | 79 +++++++++++++++++++ .../dotnet/Games.Common/IO/IReadWrite.cs | 15 ++++ 00_Common/dotnet/Games.Common/IO/TextIO.cs | 14 +++- 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 00_Common/dotnet/Games.Common.Test/IO/TextIOTests/NumberFormatTests.cs diff --git a/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/NumberFormatTests.cs b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/NumberFormatTests.cs new file mode 100644 index 00000000..670a7366 --- /dev/null +++ b/00_Common/dotnet/Games.Common.Test/IO/TextIOTests/NumberFormatTests.cs @@ -0,0 +1,79 @@ +using System; +using System.IO; +using FluentAssertions; +using Xunit; + +namespace Games.Common.IO.TextIOTests; + +public class NumberFormatTests +{ + [Theory] + [MemberData(nameof(WriteFloatTestCases))] + public void Write_Float_FormatsNumberSameAsBasic(float value, string basicString) + { + var outputWriter = new StringWriter(); + var io = new TextIO(new StringReader(""), outputWriter); + + io.Write(value); + + outputWriter.ToString().Should().BeEquivalentTo(basicString); + } + + [Theory] + [MemberData(nameof(WriteFloatTestCases))] + public void WriteLine_Float_FormatsNumberSameAsBasic(float value, string basicString) + { + var outputWriter = new StringWriter(); + var io = new TextIO(new StringReader(""), outputWriter); + + io.WriteLine(value); + + outputWriter.ToString().Should().BeEquivalentTo(basicString + Environment.NewLine); + } + + public static TheoryData WriteFloatTestCases() + => new() + { + { 1000F, " 1000 " }, + { 3.1415927F, " 3.1415927 " }, + { 1F, " 1 " }, + { 0F, " 0 " }, + { -1F, "-1 " }, + { -3.1415927F, "-3.1415927 " }, + { -1000F, "-1000 " }, + }; + + [Theory] + [MemberData(nameof(WriteIntTestCases))] + public void Write_Int_FormatsNumberSameAsBasic(int value, string basicString) + { + var outputWriter = new StringWriter(); + var io = new TextIO(new StringReader(""), outputWriter); + + io.Write(value); + + outputWriter.ToString().Should().BeEquivalentTo(basicString); + } + + [Theory] + [MemberData(nameof(WriteIntTestCases))] + public void WriteLine_Int_FormatsNumberSameAsBasic(int value, string basicString) + { + var outputWriter = new StringWriter(); + var io = new TextIO(new StringReader(""), outputWriter); + + io.WriteLine(value); + + outputWriter.ToString().Should().BeEquivalentTo(basicString + Environment.NewLine); + } + + public static TheoryData WriteIntTestCases() + => new() + { + { 1000, " 1000 " }, + { 1, " 1 " }, + { 0, " 0 " }, + { -1, "-1 " }, + { -1000, "-1000 " }, + }; +} diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs index 629b22aa..2a6433e1 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -1,3 +1,5 @@ +using System; + namespace Games.Common.IO; /// @@ -65,4 +67,17 @@ public interface IReadWrite /// /// The to be written. void WriteLine(string message); + + /// + /// Writes a to output, formatted per the BASIC interpreter, with leading and trailing spaces. + /// + /// The to be written. + void Write(float value); + + /// + /// Writes a to output, formatted per the BASIC interpreter, with leading and trailing spaces, + /// followed by a new-line. + /// + /// The to be written. + void WriteLine(float value); } diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs index 5a7cf753..9c3d6ba9 100644 --- a/00_Common/dotnet/Games.Common/IO/TextIO.cs +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -67,10 +67,6 @@ public class TextIO : IReadWrite nameof(quantity), $"'{nameof(quantity)}' must be greater than zero."); - public void Write(string value) => _output.Write(value); - - public void WriteLine(string value) => _output.WriteLine(value); - public string ReadString(string prompt) { return ReadStrings(prompt, 1)[0]; @@ -90,4 +86,14 @@ public class TextIO : IReadWrite Write(prompt + "? "); return _input.ReadLine(); } + + public void Write(string value) => _output.Write(value); + + public void WriteLine(string value) => _output.WriteLine(value); + + public void Write(float value) => _output.Write(GetString(value)); + + public void WriteLine(float value) => _output.WriteLine(GetString(value)); + + private string GetString(float value) => value < 0 ? $"{value} " : $" {value} "; } From f61350ce04f9c3837edda5f941f0f21459925e2f Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 2 Mar 2022 07:42:07 +1100 Subject: [PATCH 15/21] Remove sample program --- .../Games.Common.Sample/Games.Common.Sample.csproj | 13 ------------- 00_Common/dotnet/Games.Common.Sample/Program.cs | 7 ------- 2 files changed, 20 deletions(-) delete mode 100644 00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj delete mode 100644 00_Common/dotnet/Games.Common.Sample/Program.cs diff --git a/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj b/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj deleted file mode 100644 index a4eb9b00..00000000 --- a/00_Common/dotnet/Games.Common.Sample/Games.Common.Sample.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - Exe - net6.0 - enable - - - diff --git a/00_Common/dotnet/Games.Common.Sample/Program.cs b/00_Common/dotnet/Games.Common.Sample/Program.cs deleted file mode 100644 index 6bd2a8c4..00000000 --- a/00_Common/dotnet/Games.Common.Sample/Program.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Games.Common.IO; - -var io = new ConsoleIO(); - -var name = io.ReadString("What's your name"); - -io.WriteLine($"Hello, {name}"); From 2c1e4af70217c64fe50a22cee14d7851362235cc Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 2 Mar 2022 21:45:42 +1100 Subject: [PATCH 16/21] Add random number generator --- .../dotnet/Games.Common/Randomness/IRandom.cs | 55 +++++++++++++++++++ .../Randomness/RandomNumberGenerator.cs | 50 +++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 00_Common/dotnet/Games.Common/Randomness/IRandom.cs create mode 100644 00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs diff --git a/00_Common/dotnet/Games.Common/Randomness/IRandom.cs b/00_Common/dotnet/Games.Common/Randomness/IRandom.cs new file mode 100644 index 00000000..0ed4a587 --- /dev/null +++ b/00_Common/dotnet/Games.Common/Randomness/IRandom.cs @@ -0,0 +1,55 @@ +namespace Games.Common.Randomness; + +/// +/// Provides access to a random number generator +/// +public interface IRandom +{ + /// + /// Gets a random such that 0 <= n < 1. + /// + /// The random number. + float NextFloat(); + + /// + /// Gets a random such that 0 <= n < exclusiveMaximum. + /// + /// The random number. + float NextFloat(float exclusiveMaximum); + + /// + /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. + /// + /// The random number. + float NextFloat(float inclusiveMinimum, float exclusiveMaximum); + + /// + /// Gets a random such that 0 <= n < exclusiveMaximum. + /// + /// The random number. + int Next(int exclusiveMaximum); + + /// + /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. + /// + /// The random number. + int Next(int inclusiveMinimum, int exclusiveMaximum); + + /// + /// Gets the previous random number as a . + /// + /// The previous random number. + float PreviousFloat(); + + /// + /// Gets the previous random number as an . + /// + /// The previous random number. + int Previous(); + + /// + /// Reseeds the random number generator. + /// + /// The seed. + void Reseed(int seed); +} diff --git a/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs b/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs new file mode 100644 index 00000000..2c8749cb --- /dev/null +++ b/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs @@ -0,0 +1,50 @@ +using System; + +namespace Games.Common.Randomness; + +public class RandomNumberGenerator : IRandom +{ + private Random _random; + private float _previous; + + public RandomNumberGenerator() + { + // The BASIC RNG is seeded based on time with a 1 second resolution + _random = new Random((int)(DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond)); + } + + public float NextFloat() => NextFloat(1); + + public float NextFloat(float exclusiveMaximum) + { + if (exclusiveMaximum <= 0) + { + throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than 0."); + } + + return NextFloat(0, exclusiveMaximum); + } + + public float NextFloat(float inclusiveMinimum, float exclusiveMaximum) + { + if (exclusiveMaximum <= inclusiveMinimum) + { + throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than inclusiveMinimum."); + } + + var range = exclusiveMaximum - inclusiveMinimum; + return _previous = ((float)_random.NextDouble()) * range + inclusiveMinimum; + } + + public int Next(int exclusiveMaximum) => ToInt(NextFloat(exclusiveMaximum)); + + public int Next(int inclusiveMinimum, int exclusiveMaximum) => ToInt(NextFloat(inclusiveMinimum, exclusiveMaximum)); + + public float PreviousFloat() => _previous; + + public int Previous() => ToInt(_previous); + + private static int ToInt(float value) => (int)Math.Floor(value); + + public void Reseed(int seed) => _random = new Random(seed); +} \ No newline at end of file From 8165f7a1612ef662ecb4e30e71e11e5156efe34f Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Sat, 5 Mar 2022 15:43:03 +1100 Subject: [PATCH 17/21] Use common library in 86_Target --- 00_Common/dotnet/Games.Common.sln | 6 -- .../dotnet/Games.Common/IO/IReadWrite.cs | 9 ++- 00_Common/dotnet/Games.Common/IO/TextIO.cs | 11 +++- 86_Target/csharp/FiringRange.cs | 16 +++-- 86_Target/csharp/Game.cs | 55 +++++++++-------- 86_Target/csharp/Input.cs | 59 ------------------- 86_Target/csharp/Program.cs | 40 +++++++------ 86_Target/csharp/RandomExtensions.cs | 6 +- 86_Target/csharp/Target.csproj | 4 ++ 9 files changed, 82 insertions(+), 124 deletions(-) delete mode 100644 86_Target/csharp/Input.cs diff --git a/00_Common/dotnet/Games.Common.sln b/00_Common/dotnet/Games.Common.sln index 3498c7d1..e0f51d4b 100644 --- a/00_Common/dotnet/Games.Common.sln +++ b/00_Common/dotnet/Games.Common.sln @@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common", "Games.Commo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common.Test", "Games.Common.Test\Games.Common.Test.csproj", "{8369DA66-0414-4A14-B5BE-73B0159498A2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Games.Common.Sample", "Games.Common.Sample\Games.Common.Sample.csproj", "{395FBF0D-404E-495B-9760-8BEE3A6F5B62}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,9 +24,5 @@ Global {8369DA66-0414-4A14-B5BE-73B0159498A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {8369DA66-0414-4A14-B5BE-73B0159498A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {8369DA66-0414-4A14-B5BE-73B0159498A2}.Release|Any CPU.Build.0 = Release|Any CPU - {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Release|Any CPU.ActiveCfg = Release|Any CPU - {395FBF0D-404E-495B-9760-8BEE3A6F5B62}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs index 2a6433e1..27bd6986 100644 --- a/00_Common/dotnet/Games.Common/IO/IReadWrite.cs +++ b/00_Common/dotnet/Games.Common/IO/IReadWrite.cs @@ -1,4 +1,5 @@ using System; +using System.IO; namespace Games.Common.IO; @@ -66,7 +67,7 @@ public interface IReadWrite /// Writes a to output, followed by a new-line. /// /// The to be written. - void WriteLine(string message); + void WriteLine(string message = ""); /// /// Writes a to output, formatted per the BASIC interpreter, with leading and trailing spaces. @@ -80,4 +81,10 @@ public interface IReadWrite /// /// The to be written. void WriteLine(float value); + + /// + /// Writes the contents of a to output. + /// + /// The to be written. + void Write(Stream stream); } diff --git a/00_Common/dotnet/Games.Common/IO/TextIO.cs b/00_Common/dotnet/Games.Common/IO/TextIO.cs index 9c3d6ba9..d5e9f9f6 100644 --- a/00_Common/dotnet/Games.Common/IO/TextIO.cs +++ b/00_Common/dotnet/Games.Common/IO/TextIO.cs @@ -89,11 +89,20 @@ public class TextIO : IReadWrite public void Write(string value) => _output.Write(value); - public void WriteLine(string value) => _output.WriteLine(value); + public void WriteLine(string value = "") => _output.WriteLine(value); public void Write(float value) => _output.Write(GetString(value)); public void WriteLine(float value) => _output.WriteLine(GetString(value)); + public void Write(Stream stream) + { + using var reader = new StreamReader(stream); + while (!reader.EndOfStream) + { + _output.WriteLine(reader.ReadLine()); + } + } + private string GetString(float value) => value < 0 ? $"{value} " : $" {value} "; } diff --git a/86_Target/csharp/FiringRange.cs b/86_Target/csharp/FiringRange.cs index 368dc453..b8a6399c 100644 --- a/86_Target/csharp/FiringRange.cs +++ b/86_Target/csharp/FiringRange.cs @@ -1,25 +1,23 @@ -using System; +using Games.Common.Randomness; namespace Target { internal class FiringRange { - private readonly Random random; + private readonly IRandom _random; + private Point _targetPosition; - public FiringRange() + public FiringRange(IRandom random) { - random = new Random(); - NextTarget(); + _random = random; } - public Point TargetPosition { get; private set; } - - public void NextTarget() => TargetPosition = random.NextPosition(); + public Point NextTarget() => _targetPosition = _random.NextPosition(); public Explosion Fire(Angle angleFromX, Angle angleFromZ, float distance) { var explosionPosition = new Point(angleFromX, angleFromZ, distance); - var targetOffset = explosionPosition - TargetPosition; + var targetOffset = explosionPosition - _targetPosition; return new (explosionPosition, targetOffset); } } diff --git a/86_Target/csharp/Game.cs b/86_Target/csharp/Game.cs index 2cef015a..4bb4f9b2 100644 --- a/86_Target/csharp/Game.cs +++ b/86_Target/csharp/Game.cs @@ -1,39 +1,41 @@ using System; +using Games.Common.IO; namespace Target { internal class Game { + private readonly IReadWrite _io; private readonly FiringRange _firingRange; private int _shotCount; - private Game(FiringRange firingRange) + public Game(IReadWrite io, FiringRange firingRange) { + _io = io; _firingRange = firingRange; } - public static void Play(FiringRange firingRange) => new Game(firingRange).Play(); - - private void Play() + public void Play() { - var target = _firingRange.TargetPosition; - Console.WriteLine(target.GetBearing()); - Console.WriteLine($"Target sighted: approximate coordinates: {target}"); + _shotCount = 0; + var target = _firingRange.NextTarget(); + _io.WriteLine(target.GetBearing()); + _io.WriteLine($"Target sighted: approximate coordinates: {target}"); while (true) { - Console.WriteLine($" Estimated distance: {target.EstimateDistance()}"); - Console.WriteLine(); + _io.WriteLine($" Estimated distance: {target.EstimateDistance()}"); + _io.WriteLine(); var explosion = Shoot(); if (explosion.IsTooClose) { - Console.WriteLine("You blew yourself up!!"); + _io.WriteLine("You blew yourself up!!"); return; } - Console.WriteLine(explosion.GetBearing()); + _io.WriteLine(explosion.GetBearing()); if (explosion.IsHit) { @@ -47,31 +49,32 @@ namespace Target private Explosion Shoot() { - var input = Input.ReadNumbers("Input angle deviation from X, angle deviation from Z, distance", 3); + var (xDeviation, zDeviation, distance) = _io.Read3Numbers( + "Input angle deviation from X, angle deviation from Z, distance"); _shotCount++; - Console.WriteLine(); + _io.WriteLine(); - return _firingRange.Fire(Angle.InDegrees(input[0]), Angle.InDegrees(input[1]), input[2]); + return _firingRange.Fire(Angle.InDegrees(xDeviation), Angle.InDegrees(zDeviation), distance); } private void ReportHit(float distance) { - Console.WriteLine(); - Console.WriteLine($" * * * HIT * * * Target is non-functional"); - Console.WriteLine(); - Console.WriteLine($"Distance of explosion from target was {distance} kilometers."); - Console.WriteLine(); - Console.WriteLine($"Mission accomplished in {_shotCount} shots."); + _io.WriteLine(); + _io.WriteLine($" * * * HIT * * * Target is non-functional"); + _io.WriteLine(); + _io.WriteLine($"Distance of explosion from target was {distance} kilometers."); + _io.WriteLine(); + _io.WriteLine($"Mission accomplished in {_shotCount} shots."); } private void ReportMiss(Explosion explosion) { ReportMiss(explosion.FromTarget); - Console.WriteLine($"Approx position of explosion: {explosion.Position}"); - Console.WriteLine($" Distance from target = {explosion.DistanceToTarget}"); - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine(); + _io.WriteLine($"Approx position of explosion: {explosion.Position}"); + _io.WriteLine($" Distance from target = {explosion.DistanceToTarget}"); + _io.WriteLine(); + _io.WriteLine(); + _io.WriteLine(); } private void ReportMiss(Offset targetOffset) @@ -82,7 +85,7 @@ namespace Target } private void ReportMiss(float delta, string positiveText, string negativeText) => - Console.WriteLine(delta >= 0 ? GetOffsetText(positiveText, delta) : GetOffsetText(negativeText, -delta)); + _io.WriteLine(delta >= 0 ? GetOffsetText(positiveText, delta) : GetOffsetText(negativeText, -delta)); private static string GetOffsetText(string text, float distance) => $"Shot {text} target {distance} kilometers."; } diff --git a/86_Target/csharp/Input.cs b/86_Target/csharp/Input.cs deleted file mode 100644 index 3cfce2c0..00000000 --- a/86_Target/csharp/Input.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Target -{ - // Provides input methods which emulate the BASIC interpreter's keyboard input routines - internal static class Input - { - internal static void Prompt(string text = "") => Console.Write($"{text}? "); - - internal static List ReadNumbers(string prompt, int requiredCount) - { - var numbers = new List(); - - while (!TryReadNumbers(prompt, requiredCount, numbers)) - { - numbers.Clear(); - prompt = ""; - } - - return numbers; - } - - private static bool TryReadNumbers(string prompt, int requiredCount, List numbers) - { - Prompt(prompt); - var inputValues = ReadStrings(); - - foreach (var value in inputValues) - { - if (numbers.Count == requiredCount) - { - Console.WriteLine("!Extra input ingored"); - break; - } - - if (!TryParseNumber(value, out var number)) - { - return false; - } - - numbers.Add(number); - } - - return numbers.Count == requiredCount || TryReadNumbers("?", requiredCount, numbers); - } - - private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries); - - private static bool TryParseNumber(string text, out float number) - { - if (float.TryParse(text, out number)) { return true; } - - Console.WriteLine("!Number expected - retry input line"); - number = default; - return false; - } - } -} diff --git a/86_Target/csharp/Program.cs b/86_Target/csharp/Program.cs index b861f0e6..cafa559a 100644 --- a/86_Target/csharp/Program.cs +++ b/86_Target/csharp/Program.cs @@ -1,39 +1,43 @@ using System; using System.Reflection; +using Games.Common.IO; +using Games.Common.Randomness; namespace Target { class Program { - static void Main(string[] args) + static void Main() { - DisplayTitleAndInstructions(); + var io = new ConsoleIO(); + var game = new Game(io, new FiringRange(new RandomNumberGenerator())); - var firingRange = new FiringRange(); + Play(game, io, () => true); + } - while (true) + public static void Play(Game game, TextIO io, Func playAgain) + { + DisplayTitleAndInstructions(io); + + while (playAgain()) { - Game.Play(firingRange); + game.Play(); - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine(); - Console.WriteLine("Next target..."); - Console.WriteLine(); - - firingRange.NextTarget(); + io.WriteLine(); + io.WriteLine(); + io.WriteLine(); + io.WriteLine(); + io.WriteLine(); + io.WriteLine("Next target..."); + io.WriteLine(); } } - private static void DisplayTitleAndInstructions() + private static void DisplayTitleAndInstructions(TextIO io) { using var stream = Assembly.GetExecutingAssembly() .GetManifestResourceStream("Target.Strings.TitleAndInstructions.txt"); - using var stdout = Console.OpenStandardOutput(); - - stream.CopyTo(stdout); + io.Write(stream); } } } diff --git a/86_Target/csharp/RandomExtensions.cs b/86_Target/csharp/RandomExtensions.cs index 45443c26..a76b0279 100644 --- a/86_Target/csharp/RandomExtensions.cs +++ b/86_Target/csharp/RandomExtensions.cs @@ -1,12 +1,10 @@ -using System; +using Games.Common.Randomness; namespace Target { internal static class RandomExtensions { - public static float NextFloat(this Random rnd) => (float)rnd.NextDouble(); - - public static Point NextPosition(this Random rnd) => new ( + public static Point NextPosition(this IRandom rnd) => new ( Angle.InRotations(rnd.NextFloat()), Angle.InRotations(rnd.NextFloat()), 100000 * rnd.NextFloat() + rnd.NextFloat()); diff --git a/86_Target/csharp/Target.csproj b/86_Target/csharp/Target.csproj index 2df33b67..ea8ff526 100644 --- a/86_Target/csharp/Target.csproj +++ b/86_Target/csharp/Target.csproj @@ -9,4 +9,8 @@ + + + + From 5aaf703bf49390e6aa1d5df73c0dd248eced177e Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 7 Mar 2022 22:03:45 +1100 Subject: [PATCH 18/21] Improve Random interface --- .../dotnet/Games.Common/Randomness/IRandom.cs | 34 +------ .../Randomness/IRandomExtensions.cs | 91 +++++++++++++++++++ .../Randomness/RandomNumberGenerator.cs | 34 +------ 3 files changed, 96 insertions(+), 63 deletions(-) create mode 100644 00_Common/dotnet/Games.Common/Randomness/IRandomExtensions.cs diff --git a/00_Common/dotnet/Games.Common/Randomness/IRandom.cs b/00_Common/dotnet/Games.Common/Randomness/IRandom.cs index 0ed4a587..9db4c12f 100644 --- a/00_Common/dotnet/Games.Common/Randomness/IRandom.cs +++ b/00_Common/dotnet/Games.Common/Randomness/IRandom.cs @@ -6,47 +6,17 @@ namespace Games.Common.Randomness; public interface IRandom { /// - /// Gets a random such that 0 <= n < 1. + /// Gets a random such that 0 <= n < 1. /// /// The random number. float NextFloat(); /// - /// Gets a random such that 0 <= n < exclusiveMaximum. - /// - /// The random number. - float NextFloat(float exclusiveMaximum); - - /// - /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. - /// - /// The random number. - float NextFloat(float inclusiveMinimum, float exclusiveMaximum); - - /// - /// Gets a random such that 0 <= n < exclusiveMaximum. - /// - /// The random number. - int Next(int exclusiveMaximum); - - /// - /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. - /// - /// The random number. - int Next(int inclusiveMinimum, int exclusiveMaximum); - - /// - /// Gets the previous random number as a . + /// Gets the returned by the previous call to . /// /// The previous random number. float PreviousFloat(); - /// - /// Gets the previous random number as an . - /// - /// The previous random number. - int Previous(); - /// /// Reseeds the random number generator. /// diff --git a/00_Common/dotnet/Games.Common/Randomness/IRandomExtensions.cs b/00_Common/dotnet/Games.Common/Randomness/IRandomExtensions.cs new file mode 100644 index 00000000..bfdd0b49 --- /dev/null +++ b/00_Common/dotnet/Games.Common/Randomness/IRandomExtensions.cs @@ -0,0 +1,91 @@ +using System; + +namespace Games.Common.Randomness; + +/// +/// Provides extension methods to providing random numbers in a given range. +/// +/// +public static class IRandomExtensions +{ + /// + /// Gets a random such that 0 <= n < exclusiveMaximum. + /// + /// The random number. + public static float NextFloat(this IRandom random, float exclusiveMaximum) => + Scale(random.NextFloat(), exclusiveMaximum); + + /// + /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. + /// + /// The random number. + public static float NextFloat(this IRandom random, float inclusiveMinimum, float exclusiveMaximum) => + Scale(random.NextFloat(), inclusiveMinimum, exclusiveMaximum); + + /// + /// Gets a random such that 0 <= n < exclusiveMaximum. + /// + /// The random number. + public static int Next(this IRandom random, int exclusiveMaximum) => ToInt(random.NextFloat(exclusiveMaximum)); + + /// + /// Gets a random such that inclusiveMinimum <= n < exclusiveMaximum. + /// + /// The random number. + public static int Next(this IRandom random, int inclusiveMinimum, int exclusiveMaximum) => + ToInt(random.NextFloat(inclusiveMinimum, exclusiveMaximum)); + + /// + /// Gets the previous unscaled (between 0 and 1) scaled to a new range: + /// 0 <= x < . + /// + /// The random number. + public static float PreviousFloat(this IRandom random, float exclusiveMaximum) => + Scale(random.PreviousFloat(), exclusiveMaximum); + + /// + /// Gets the previous unscaled (between 0 and 1) scaled to a new range: + /// <= n < . + /// + /// The random number. + public static float PreviousFloat(this IRandom random, float inclusiveMinimum, float exclusiveMaximum) => + Scale(random.PreviousFloat(), inclusiveMinimum, exclusiveMaximum); + + /// + /// Gets the previous unscaled (between 0 and 1) scaled to an in a new + /// range: 0 <= n < . + /// + /// The random number. + public static int Previous(this IRandom random, int exclusiveMaximum) => + ToInt(random.PreviousFloat(exclusiveMaximum)); + + /// + /// Gets the previous unscaled (between 0 and 1) scaled to an in a new + /// range: <= n < . + /// The random number. + public static int Previous(this IRandom random, int inclusiveMinimum, int exclusiveMaximum) => + ToInt(random.PreviousFloat(inclusiveMinimum, exclusiveMaximum)); + + private static float Scale(float zeroToOne, float exclusiveMaximum) + { + if (exclusiveMaximum <= 0) + { + throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than 0."); + } + + return Scale(zeroToOne, 0, exclusiveMaximum); + } + + private static float Scale(float zeroToOne, float inclusiveMinimum, float exclusiveMaximum) + { + if (exclusiveMaximum <= inclusiveMinimum) + { + throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than inclusiveMinimum."); + } + + var range = exclusiveMaximum - inclusiveMinimum; + return zeroToOne * range + inclusiveMinimum; + } + + private static int ToInt(float value) => (int)Math.Floor(value); +} \ No newline at end of file diff --git a/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs b/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs index 2c8749cb..d46f72e7 100644 --- a/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs +++ b/00_Common/dotnet/Games.Common/Randomness/RandomNumberGenerator.cs @@ -2,6 +2,7 @@ using System; namespace Games.Common.Randomness; +/// public class RandomNumberGenerator : IRandom { private Random _random; @@ -13,38 +14,9 @@ public class RandomNumberGenerator : IRandom _random = new Random((int)(DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond)); } - public float NextFloat() => NextFloat(1); - - public float NextFloat(float exclusiveMaximum) - { - if (exclusiveMaximum <= 0) - { - throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than 0."); - } - - return NextFloat(0, exclusiveMaximum); - } - - public float NextFloat(float inclusiveMinimum, float exclusiveMaximum) - { - if (exclusiveMaximum <= inclusiveMinimum) - { - throw new ArgumentOutOfRangeException(nameof(exclusiveMaximum), "Must be greater than inclusiveMinimum."); - } - - var range = exclusiveMaximum - inclusiveMinimum; - return _previous = ((float)_random.NextDouble()) * range + inclusiveMinimum; - } - - public int Next(int exclusiveMaximum) => ToInt(NextFloat(exclusiveMaximum)); - - public int Next(int inclusiveMinimum, int exclusiveMaximum) => ToInt(NextFloat(inclusiveMinimum, exclusiveMaximum)); + public float NextFloat() => _previous = (float)_random.NextDouble(); public float PreviousFloat() => _previous; - public int Previous() => ToInt(_previous); - - private static int ToInt(float value) => (int)Math.Floor(value); - public void Reseed(int seed) => _random = new Random(seed); -} \ No newline at end of file +} From 351533b07dda160286a37516dfee8d3808012626 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 7 Mar 2022 22:04:01 +1100 Subject: [PATCH 19/21] Add top-level README --- 00_Common/BASIC_Tests/InputTest.bas | 5 + 00_Common/BASIC_Tests/OutputTest.bas | 4 + 00_Common/BASIC_Tests/RndTest.bas | 6 + 00_Common/README.md | 182 +++++++++++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 00_Common/BASIC_Tests/InputTest.bas create mode 100644 00_Common/BASIC_Tests/OutputTest.bas create mode 100644 00_Common/BASIC_Tests/RndTest.bas create mode 100644 00_Common/README.md diff --git a/00_Common/BASIC_Tests/InputTest.bas b/00_Common/BASIC_Tests/InputTest.bas new file mode 100644 index 00000000..bc57e8bd --- /dev/null +++ b/00_Common/BASIC_Tests/InputTest.bas @@ -0,0 +1,5 @@ +10 INPUT "Enter 3 numbers";A,B,C +20 PRINT "You entered: ";A;B;C +30 PRINT "--------------------------" +40 GOTO 10 + diff --git a/00_Common/BASIC_Tests/OutputTest.bas b/00_Common/BASIC_Tests/OutputTest.bas new file mode 100644 index 00000000..b857401f --- /dev/null +++ b/00_Common/BASIC_Tests/OutputTest.bas @@ -0,0 +1,4 @@ +10 A=1: B=-2: C=0.7: D=123456789: E=-0.0000000001 +20 PRINT "|";A;"|";B;"|";C;"|";D;"|";E;"|" + + diff --git a/00_Common/BASIC_Tests/RndTest.bas b/00_Common/BASIC_Tests/RndTest.bas new file mode 100644 index 00000000..f4702f19 --- /dev/null +++ b/00_Common/BASIC_Tests/RndTest.bas @@ -0,0 +1,6 @@ +10 PRINT "1: ";RND(1);RND(1);RND(0);RND(0);RND(1) +20 PRINT "2: ";RND(-2);RND(1);RND(1);RND(1) +30 PRINT "3: ";RND(-5);RND(1);RND(1);RND(1) +40 PRINT "4: ";RND(-2);RND(1);RND(1);RND(1) + + diff --git a/00_Common/README.md b/00_Common/README.md new file mode 100644 index 00000000..81a0b8e0 --- /dev/null +++ b/00_Common/README.md @@ -0,0 +1,182 @@ +# Common Library + +## Purpose + +The primary purpose of this library is to implement common behaviours of the BASIC interpreter that impact gameplay, to +free coders porting the games to concentrate on the explicit game logic. + +The behaviours implemented by this library are: + +* Complex interactions involved in text input. +* Formatting of number in text output. +* Behaviour of the BASIC `RND(float)` PRNG function. + +A secondary purpose is to provide common services that, with dependency injection, would allow a ported game to be +driven programmatically to permit full-game acceptance tests to be written. + +The library is **NOT** intended to be: + +* a repository for common game logic that is implemented in the BASIC code of the games; or +* a DSL allowing the BASIC code to be compiled in a different language environment with minimal changes. This implies + that implementations of the above behaviours should use method names, etc, that are idiomatic of the specific + language's normal routines for that behaviour. + +## Text Input + +The behaviour of the BASIC interpreter when accepting text input from the user is a major element of the original +gameplay experience that is seen to be valuable to maintain. This behaviour is complex and non-trivial to implement, and +is better implemented once for other developers to use so they can concentrate on the explicit game logic. + +The text input/output behaviour can be investigated using a basic program such as: + +**`BASIC_Tests/InputTest.bas`** + +```basic +10 INPUT "Enter 3 numbers";A,B,C +20 PRINT "You entered: ";A;B;C +30 PRINT "--------------------------" +40 GOTO 10 +``` + +The following transcript shows the use of this program, and some interesting behaviours of the BASIC interpreter INPUT +routine. There are some other behaviours which can be seen in the unit tests for the C# library implementation. + +```dos +Enter 3 numbers? -1,2,3.141 <-- multiple numbers are separated by commas +You entered: -1 2 3.141 +-------------------------- +Enter 3 numbers? 1 <-- ... or entered on separate lines +?? 2 +?? 3 +You entered: 1 2 3 +-------------------------- +Enter 3 numbers? 1,2 <-- ... or both +?? 3 +You entered: 1 2 3 +-------------------------- +Enter 3 numbers? 1,-2,3,4 <-- Extra input is ignore with a warning +!EXTRA INPUT IGNORED +You entered: 1 -2 3 +-------------------------- +Enter 3 numbers? 5 , 6.7, -8 ,10 <-- Whitespace around values is ignored +!EXTRA INPUT IGNORED +You entered: 5 6.7 -8 +-------------------------- +Enter 3 numbers? abcd,e,f <-- Non-numeric entries must be retried +!NUMBER EXPECTED - RETRY INPUT LINE +? 1,2,abc <-- A single non-numeric invalidates the whole line +!NUMBER EXPECTED - RETRY INPUT LINE +? 1de,2.3f,10k,abcde <-- ... except for trailing non-digit chars and extra input +!EXTRA INPUT IGNORED +You entered: 1 2.3 10 +-------------------------- +Enter 3 numbers? 1,"2,3",4 <-- Double-quotes enclose a single parsed value +You entered: 1 2 4 +-------------------------- +Enter 3 numbers? 1,2,"3 <-- An unmatched double-quote crashes the interpreter +vintbas.exe: Mismatched inputbuf in inputVars +CallStack (from HasCallStack): + error, called at src\Language\VintageBasic\Interpreter.hs:436:21 in main:Language.VintageBasic.Interpreter +``` + +I propose to ignore this last behaviour - the interpreter crash - and instead treat the end of the input line as the end +of a quoted value. There are some additional behaviours to those shown above which can be seen in the unit tests for +the C# implementation of the library. + +### Implementation Notes + +The usage of the `INPUT` command in the BASIC code of the games was analysed, with the variables used designated as +`num`, for a numeric variable (eg. `M1`), or `str`, for a string variable (eg. `C$`). The result of this usage analysis +across all game programs is: + +Variable number and type|Count +---|--- +str|137 +str str|2 +num|187 +num num|27 +num num num|7 +num num num num|1 +num num num num num num num num num num|1 + +The usage count is interesting, but not important. What is important is the variable type and number for each usage. +Implementers if the above behaviours do not need to cater for mixed variable types in their input routines (although the +BASIC interpreter does support this). Input routines also need to cater only for 1 or 2 string values, and 1, 2, 3, 4, +or 10 numeric values. + +## Numbers in Text Output + +As seen in the transcript above, the BASIC interpreter has some particular rules for formatting numbers in text output. +Negative numbers are printed with a leading negative sign (`-`) and a trailing space. Positive numbers are printed also +with the trailing space, but with a leading space in place of the sign character. + +Additional formatting rules can be seen by running this program: + +**`BASIC_Tests/OutputTest.bas`** + +```basic +10 A=1: B=-2: C=0.7: D=123456789: E=-0.0000000001 +20 PRINT "|";A;"|";B;"|";C;"|";D;"|";E;"|" +``` + +The output is: + +```dos +| 1 |-2 | .7 | 1.2345679E+8 |-1.E-10 | +``` + +This clearly shows the leading and trailing spaces, but also shows that: + +* numbers without an integer component are printed without a leading zero to the left of the decimal place; +* numbers above and below a certain magnitude are printed in scientific notation. + + +### Implementation Notes + + +I think the important piece of this number formatting behaviour, in terms of its impact on replicating the text output +of the original games, is the leading and trailing spaces. This should be the minimum behaviour supported for numeric +output. Additional formatting behaviours may be investigated and supported by library implementers as they choose. + +## The BASIC `RND(float)` function + +The [Vintage BASIC documentation](http://vintage-basic.net/downloads/Vintage_BASIC_Users_Guide.html) describes the +behaviour of the `RND(float)` function: + +> Psuedorandom number generator. The behavior is different depending on the value passed. If the value is positive, the +> result will be a new random value between 0 and 1 (including 0 but not 1). If the value is zero, the result will be a +> repeat of the last random number generated. If the value is negative, it will be rounded down to the nearest integer +> and used to reseed the random number generator. Pseudorandom sequences can be repeated by reseeding with the same +> number. + +This behaviour can be shown by the program: + +**`BASIC_Tests/RndTest.bas`** + +```basic +10 PRINT "1: ";RND(1);RND(1);RND(0);RND(0);RND(1) +20 PRINT "2: ";RND(-2);RND(1);RND(1);RND(1) +30 PRINT "3: ";RND(-5);RND(1);RND(1);RND(1) +40 PRINT "4: ";RND(-2);RND(1);RND(1);RND(1) +``` + +The output of this program is: + +```dos +1: .97369444 .44256502 .44256502 .44256502 .28549057 <-- Repeated values due to RND(0) +2: .4986506 4.4510484E-2 .96231 .35997057 +3: .8113741 .13687313 6.1034977E-2 .7874807 +4: .4986506 4.4510484E-2 .96231 .35997057 <-- Same sequence as line 2 due to same seed +``` + + +### Implementation Notes + + +While the BASIC `RND(x)` function always returns a number between 0 (inclusive) and 1 (exclusive) for positive non-zero +values of `x`, game porters would find it convenient for the library to include functions returning a random float or +integer in a range from an inclusive minimum to an exclusive maximum. + +As one of the games, "Football", makes use of `RND(0)` with different scaling applied than the previous use of `RND(1)`, +a common library implementation should always generate a value between 0 and 1, and scale that for a function with a +range, so that a call to the equivalent of the `RND(0)` function can return the previous value between 0 and 1. From 4fa74a10e4ae87ce58890a1265c3c5674b76a22e Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 9 Mar 2022 21:46:43 +1100 Subject: [PATCH 20/21] Add .Net usage README --- 00_Common/README.md | 3 + 00_Common/dotnet/README.md | 181 +++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 00_Common/dotnet/README.md diff --git a/00_Common/README.md b/00_Common/README.md index 81a0b8e0..5b207e05 100644 --- a/00_Common/README.md +++ b/00_Common/README.md @@ -83,6 +83,9 @@ I propose to ignore this last behaviour - the interpreter crash - and instead tr of a quoted value. There are some additional behaviours to those shown above which can be seen in the unit tests for the C# implementation of the library. +Note also that BASIC numeric variables store a single-precision floating point value, so numeric input functions should +return a value of that type. + ### Implementation Notes The usage of the `INPUT` command in the BASIC code of the games was analysed, with the variables used designated as diff --git a/00_Common/dotnet/README.md b/00_Common/dotnet/README.md new file mode 100644 index 00000000..ac0b1bcc --- /dev/null +++ b/00_Common/dotnet/README.md @@ -0,0 +1,181 @@ +# Games.Common + +This is the common library for C# and VB.Net ports of the games. + +## Overview + +### Game Input/Output + +* `TextIO` is the main class which manages text input and output for a game. It take a `TextReader` and a `TextWriter` in +its constructor so it can be wired up in unit tests to test gameplay scenarios. +* `ConsoleIO` derives from `TextIO` and binds it to `System.Console.In` and `System.Console.Out`. +* `IReadWrite` is an interface implemented by `TextIO` which may be useful in some test scenarios. + +```csharp +public interface IReadWrite +{ + // Reads a float value from input. + float ReadNumber(string prompt); + + // Reads 2 float values from input. + (float, float) Read2Numbers(string prompt); + + // Reads 3 float values from input. + (float, float, float) Read3Numbers(string prompt); + + // Reads 4 float values from input. + (float, float, float, float) Read4Numbers(string prompt); + + // Read numbers from input to fill an array. + void ReadNumbers(string prompt, float[] values); + + // Reads a string value from input. + string ReadString(string prompt); + + // Reads 2 string values from input. + (string, string) Read2Strings(string prompt); + + // Writes a string to output. + void Write(string message); + + // Writes a string to output, followed by a new-line. + void WriteLine(string message = ""); + + // Writes a float to output, formatted per the BASIC interpreter, with leading and trailing spaces. + void Write(float value); + + // Writes a float to output, formatted per the BASIC interpreter, with leading and trailing spaces, + // followed by a new-line. + void WriteLine(float value); + + // Writes the contents of a Stream to output. + void Write(Stream stream);} +``` + +### Random Number Generation + +* `IRandom` is an interface that provides basic methods that parallel the 3 uses of BASIC's `RND(float)` function. +* `RandomNumberGenerator` is an implementation of `IRandom` built around `System.Random`. +* `IRandomExtensions` provides convenience extension methods for obtaining random numbers as `int` and also within a + given range. + +```csharp +public interface IRandom +{ + // Like RND(1), gets a random float such that 0 <= n < 1. + float NextFloat(); + + // Like RND(0), Gets the float returned by the previous call to NextFloat. + float PreviousFloat(); + + // Like RND(-x), Reseeds the random number generator. + void Reseed(int seed); +} +``` + +Extension methods on `IRandom`: + +```csharp +// Gets a random float such that 0 <= n < exclusiveMaximum. +float NextFloat(this IRandom random, float exclusiveMaximum); + +// Gets a random float such that inclusiveMinimum <= n < exclusiveMaximum. +float NextFloat(this IRandom random, float inclusiveMinimum, float exclusiveMaximum); + +// Gets a random int such that 0 <= n < exclusiveMaximum. +int Next(this IRandom random, int exclusiveMaximum); + +// Gets a random int such that inclusiveMinimum <= n < exclusiveMaximum. +int Next(this IRandom random, int inclusiveMinimum, int exclusiveMaximum); + +// Gets the previous unscaled float (between 0 and 1) scaled to a new range: +// 0 <= x < exclusiveMaximum. +float PreviousFloat(this IRandom random, float exclusiveMaximum); + +// Gets the previous unscaled float (between 0 and 1) scaled to a new range: +// inclusiveMinimum <= n < exclusiveMaximum. +float PreviousFloat(this IRandom random, float inclusiveMinimum, float exclusiveMaximum); + +// Gets the previous unscaled float (between 0 and 1) scaled to an int in a new range: +// 0 <= n < exclusiveMaximum. +int Previous(this IRandom random, int exclusiveMaximum); + +// Gets the previous unscaled float (between 0 and 1) scaled to an int in a new range: +// inclusiveMinimum <= n < exclusiveMaximum. +int Previous(this IRandom random, int inclusiveMinimum, int exclusiveMaximum); +``` + +## C\# Usage + +### Add Project Reference + +Add the `Games.Common` project as a reference to the game project. For example, here's the reference from the C\# port +of `86_Tower`: + +```xml + + + +``` + +### C# Game Input/Output usage + +A game can be encapsulated in a class which takes a `TextIO` instance in it's constructor: + +```csharp +public class Game +{ + private readonly TextIO _io; + + public Game(TextIO io) => _io = io; + + public void Play() + { + var name = _io.ReadString("What is your name"); + var (cats, dogs) = _io.Read2Number($"Hello, {name}, how many pets do you have (cats, dogs)"); + _io.WriteLine($"So, {cats + dogs} pets in total, huh?"); + } +} +``` + +Then the entry point of the game program would look something like: + +```csharp +var game = new Game(new ConsoleIO()); +game.Play(); +``` + +### C# Random Number Generator usage + +```csharp +var io = new ConsoleIO(); +var rng = new RandomNumberGenerator(); +io.WriteLine(rng.NextFloat()); // 0.1234, for example +io.WriteLine(rng.NextFloat()); // 0.6, for example +io.WriteLine(rng.PreviousFloat()); // 0.6, repeats previous +io.WriteLine(rng.PreviousFloat(0, 10)); // 6, repeats previous value, but scaled to new range +``` + +### C# Unit Test usage + +`TextIO` can be initialised with a `StringReader` and `StringWriter` to enable testing. For example, given the `Game` +class above: + +```csharp +var reader = new StringReader("Joe Bloggs\r\n4\n\r5"); +var writer = new StringWriter(); +var game = new Game(new TextIO(reader, writer)) + +game.Play(); + +writer.ToString().Should().BeEquivalentTo( + "What is your name? Hello, Joe Bloggs, how many pets do you have (cats, dogs)? ?? So, 9 pets in total, huh?"); +``` + +Note the lack of line breaks in the expected output, because during game play the line breaks come from the text input. + +Of course, `IReadWrite` can also be mocked for simple test scenarios. + +## VB.Net Usage + +*To be provided* From c95851e69f76596d006f0f077414a50c189625fe Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 9 Mar 2022 22:04:07 +1100 Subject: [PATCH 21/21] Fix typo --- 00_Common/dotnet/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/00_Common/dotnet/README.md b/00_Common/dotnet/README.md index ac0b1bcc..c44f210b 100644 --- a/00_Common/dotnet/README.md +++ b/00_Common/dotnet/README.md @@ -110,7 +110,7 @@ int Previous(this IRandom random, int inclusiveMinimum, int exclusiveMaximum); ### Add Project Reference Add the `Games.Common` project as a reference to the game project. For example, here's the reference from the C\# port -of `86_Tower`: +of `86_Target`: ```xml