From f27908a3e4720ebc6a2becab4db51616f44bd861 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Thu, 29 Sep 2022 22:46:10 +1000 Subject: [PATCH 01/21] Implement poetry generator --- 70_Poetry/csharp/Context.cs | 11 +++++ 70_Poetry/csharp/IOExtensions.cs | 8 +++ 70_Poetry/csharp/Phrase.cs | 83 ++++++++++++++++++++++++++++++++ 70_Poetry/csharp/Poem.cs | 56 +++++++++++++++++++++ 70_Poetry/csharp/Poetry.csproj | 3 ++ 70_Poetry/csharp/Program.cs | 5 ++ 6 files changed, 166 insertions(+) create mode 100644 70_Poetry/csharp/Context.cs create mode 100644 70_Poetry/csharp/IOExtensions.cs create mode 100644 70_Poetry/csharp/Phrase.cs create mode 100644 70_Poetry/csharp/Poem.cs create mode 100644 70_Poetry/csharp/Program.cs diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs new file mode 100644 index 00000000..f9aecc06 --- /dev/null +++ b/70_Poetry/csharp/Context.cs @@ -0,0 +1,11 @@ +namespace Poetry; + +internal class Context +{ + public int U { get; set; } + public int I { get; set; } + public int J { get; set; } + public int K { get; set; } + public bool SkipComma { get; set; } + public bool UseGroup2 { get; set; } +} diff --git a/70_Poetry/csharp/IOExtensions.cs b/70_Poetry/csharp/IOExtensions.cs new file mode 100644 index 00000000..a2089264 --- /dev/null +++ b/70_Poetry/csharp/IOExtensions.cs @@ -0,0 +1,8 @@ +namespace Poetry; + +internal static class IOExtensions +{ + + internal static void WritePhrase(this IReadWrite io, Context context) + => Phrase.GetPhrase(context).Write(io, context); +} diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs new file mode 100644 index 00000000..1df6a774 --- /dev/null +++ b/70_Poetry/csharp/Phrase.cs @@ -0,0 +1,83 @@ +namespace Poetry; + +internal class Phrase +{ + private static Phrase[][] _phrases = new Phrase[][] + { + new Phrase[] + { + new("midnight dreary"), + new("fiery eyes"), + new("bird or fiend"), + new("thing of evil"), + new("prophet") + }, + new Phrase[] + { + new("beguiling me", ctx => ctx.U = 2), + new("thrilled me"), + new("still sitting....", ctx => ctx.SkipComma = true), + new("never flitting", ctx => ctx.U = 2), + new("burned") + }, + new Phrase[] + { + new("and my soul"), + new("darkness there"), + new("shall be lifted"), + new("quoth the raven"), + new(ctx => ctx.U != 0, "sign of parting") + }, + new Phrase[] + { + new("nothing more"), + new("yet again"), + new("slowly creeping"), + new("...evermore"), + new("nevermore") + } + }; + + private readonly Predicate _condition; + private readonly string _text; + private readonly Action _update; + + private Phrase(Predicate condition, string text) + : this(condition, text, _ => { }) + { + } + + private Phrase(string text, Action update) + : this(_ => true, text, update) + { + } + + private Phrase(string text) + : this(_ => true, text, _ => { }) + { + } + + private Phrase(Predicate condition, string text, Action update) + { + _condition = condition; + _text = text; + _update = update; + } + + public static Phrase GetPhrase(Context context) + { + var group = context.UseGroup2 ? _phrases[1] : _phrases[context.J]; + context.UseGroup2 = false; + return group[context.I % 5]; + } + + public void Write(IReadWrite io, Context context) + { + if (_condition.Invoke(context)) + { + io.Write(_text); + } + + _update.Invoke(context); + } +} \ No newline at end of file diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs new file mode 100644 index 00000000..ed08d30c --- /dev/null +++ b/70_Poetry/csharp/Poem.cs @@ -0,0 +1,56 @@ +namespace Poetry; + +internal class Poem +{ + internal static void Compose(IReadWrite io, IRandom random) + { + var context = new Context(); + + while (true) + { + io.WritePhrase(context); + + if (!context.SkipComma && context.U != 0 && random.NextFloat() <= 0.19) + { + io.Write(","); + context.U = 2; + } + + if (random.NextFloat() <= 0.65) + { + io.Write(" "); + context.U += 1; + } + else + { + io.WriteLine(); + context.U = 0; + } + + while (true) + { + context.I = random.Next(1, 6); + context.J += 1; + context.K += 1; + + if (context.U == 0 && context.J % 2 == 0) + { + io.Write(" "); + } + + if (context.J < 4) { break; } + + context.J = 0; + io.WriteLine(); + + if (context.K > 20) + { + io.WriteLine(); + context.U = context.K = 0; + context.UseGroup2 = true; + break; + } + } + } + } +} \ No newline at end of file diff --git a/70_Poetry/csharp/Poetry.csproj b/70_Poetry/csharp/Poetry.csproj index d3fe4757..6d06ca76 100644 --- a/70_Poetry/csharp/Poetry.csproj +++ b/70_Poetry/csharp/Poetry.csproj @@ -6,4 +6,7 @@ enable enable + + + diff --git a/70_Poetry/csharp/Program.cs b/70_Poetry/csharp/Program.cs new file mode 100644 index 00000000..8920b37e --- /dev/null +++ b/70_Poetry/csharp/Program.cs @@ -0,0 +1,5 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; +global using Poetry; + +Poem.Compose(new ConsoleIO(), new RandomNumberGenerator()); \ No newline at end of file From 617053b1f4ae7d05204b03e94fbb3cde89d957e5 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 3 Oct 2022 21:22:44 +1100 Subject: [PATCH 02/21] Debug poem generation --- 70_Poetry/csharp/Context.cs | 4 ++-- 70_Poetry/csharp/Phrase.cs | 7 +++++-- 70_Poetry/csharp/Poem.cs | 7 ++++--- 70_Poetry/csharp/Program.cs | 2 +- 70_Poetry/poetry.bas | 15 ++++++++++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs index f9aecc06..0036b52a 100644 --- a/70_Poetry/csharp/Context.cs +++ b/70_Poetry/csharp/Context.cs @@ -2,10 +2,10 @@ namespace Poetry; internal class Context { - public int U { get; set; } public int I { get; set; } - public int J { get; set; } + public int J { get; set; } public int K { get; set; } + public int U { get; set; } public bool SkipComma { get; set; } public bool UseGroup2 { get; set; } } diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs index 1df6a774..3f00abae 100644 --- a/70_Poetry/csharp/Phrase.cs +++ b/70_Poetry/csharp/Phrase.cs @@ -66,11 +66,14 @@ internal class Phrase public static Phrase GetPhrase(Context context) { - var group = context.UseGroup2 ? _phrases[1] : _phrases[context.J]; + var group = GetGroup(context.UseGroup2 ? 2 : context.J); context.UseGroup2 = false; - return group[context.I % 5]; + return group[Math.Max(context.I - 1, 0)]; } + private static Phrase[] GetGroup(int groupNumber) => _phrases[Math.Max(groupNumber - 1, 0)]; + + public void Write(IReadWrite io, Context context) { if (_condition.Invoke(context)) diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs index ed08d30c..75f24532 100644 --- a/70_Poetry/csharp/Poem.cs +++ b/70_Poetry/csharp/Poem.cs @@ -10,13 +10,14 @@ internal class Poem { io.WritePhrase(context); - if (!context.SkipComma && context.U != 0 && random.NextFloat() <= 0.19) + if (!context.SkipComma && random.NextFloat() <= 0.19F && context.U != 0) { io.Write(","); context.U = 2; } + context.SkipComma = false; - if (random.NextFloat() <= 0.65) + if (random.NextFloat() <= 0.65F) { io.Write(" "); context.U += 1; @@ -38,7 +39,7 @@ internal class Poem io.Write(" "); } - if (context.J < 4) { break; } + if (context.J < 5) { break; } context.J = 0; io.WriteLine(); diff --git a/70_Poetry/csharp/Program.cs b/70_Poetry/csharp/Program.cs index 8920b37e..3a17d72c 100644 --- a/70_Poetry/csharp/Program.cs +++ b/70_Poetry/csharp/Program.cs @@ -2,4 +2,4 @@ global using Games.Common.IO; global using Games.Common.Randomness; global using Poetry; -Poem.Compose(new ConsoleIO(), new RandomNumberGenerator()); \ No newline at end of file +Poem.Compose(new ConsoleIO(), new RandomNumberGenerator()); diff --git a/70_Poetry/poetry.bas b/70_Poetry/poetry.bas index 3661287d..7fa3a1c4 100644 --- a/70_Poetry/poetry.bas +++ b/70_Poetry/poetry.bas @@ -1,3 +1,8 @@ +5 Y=RND(-1) +6 REM FOR X = 1 TO 100 +7 REM PRINT RND(1);"," +8 REM NEXT X +9 REM GOTO 999 10 PRINT TAB(30);"POETRY" 20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY" 30 PRINT:PRINT:PRINT @@ -26,17 +31,21 @@ 133 PRINT "SLOWLY CREEPING";:GOTO 210 134 PRINT "...EVERMORE";:GOTO 210 135 PRINT "NEVERMORE"; -210 IF U=0 OR RND(1)>.19 THEN 212 +210 GOSUB 500 : IF U=0 OR X>.19 THEN 212 211 PRINT ",";:U=2 -212 IF RND(1)>.65 THEN 214 +212 GOSUB 500 : IF X>.65 THEN 214 213 PRINT " ";:U=U+1:GOTO 215 214 PRINT : U=0 -215 I=INT(INT(10*RND(1))/2)+1 +215 GOSUB 500 : I=INT(INT(10*X)/2)+1 220 J=J+1 : K=K+1 +225 REM PRINT "I=";I;"; J=";J;"; K=";K;"; U=";U 230 IF U>0 OR INT(J/2)<>J/2 THEN 240 235 PRINT " "; 240 ON J GOTO 90,110,120,130,250 250 J=0 : PRINT : IF K>20 THEN 270 260 GOTO 215 270 PRINT : U=0 : K=0 : GOTO 110 +500 X = RND(1) +505 REM PRINT "#";X;"#" +510 RETURN 999 END From d16d72965f4c983797b2092c361ca89bb8a97e0d Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 3 Oct 2022 21:29:34 +1100 Subject: [PATCH 03/21] Add title --- 70_Poetry/csharp/IOExtensions.cs | 1 - 70_Poetry/csharp/Poem.cs | 4 ++++ 70_Poetry/csharp/Poetry.csproj | 5 +++++ 70_Poetry/csharp/Resources/Resource.cs | 16 ++++++++++++++++ 70_Poetry/csharp/Resources/Title.txt | 5 +++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 70_Poetry/csharp/Resources/Resource.cs create mode 100644 70_Poetry/csharp/Resources/Title.txt diff --git a/70_Poetry/csharp/IOExtensions.cs b/70_Poetry/csharp/IOExtensions.cs index a2089264..631c80f8 100644 --- a/70_Poetry/csharp/IOExtensions.cs +++ b/70_Poetry/csharp/IOExtensions.cs @@ -2,7 +2,6 @@ namespace Poetry; internal static class IOExtensions { - internal static void WritePhrase(this IReadWrite io, Context context) => Phrase.GetPhrase(context).Write(io, context); } diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs index 75f24532..64038963 100644 --- a/70_Poetry/csharp/Poem.cs +++ b/70_Poetry/csharp/Poem.cs @@ -1,9 +1,13 @@ +using static Poetry.Resources.Resource; + namespace Poetry; internal class Poem { internal static void Compose(IReadWrite io, IRandom random) { + io.Write(Streams.Title); + var context = new Context(); while (true) diff --git a/70_Poetry/csharp/Poetry.csproj b/70_Poetry/csharp/Poetry.csproj index 6d06ca76..3870320c 100644 --- a/70_Poetry/csharp/Poetry.csproj +++ b/70_Poetry/csharp/Poetry.csproj @@ -6,6 +6,11 @@ enable enable + + + + + diff --git a/70_Poetry/csharp/Resources/Resource.cs b/70_Poetry/csharp/Resources/Resource.cs new file mode 100644 index 00000000..b789f035 --- /dev/null +++ b/70_Poetry/csharp/Resources/Resource.cs @@ -0,0 +1,16 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Poetry.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Title => GetStream(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/70_Poetry/csharp/Resources/Title.txt b/70_Poetry/csharp/Resources/Title.txt new file mode 100644 index 00000000..86161340 --- /dev/null +++ b/70_Poetry/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Poetry + Creative Computing Morristown, New Jersey + + + From 0c9d3580f52cc0ce77c950ff9092b4e71be71f4a Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 3 Oct 2022 22:00:42 +1100 Subject: [PATCH 04/21] Move output to context --- 70_Poetry/csharp/Context.cs | 76 ++++++++++++++++++++++++++++++++ 70_Poetry/csharp/IOExtensions.cs | 7 --- 70_Poetry/csharp/Phrase.cs | 3 +- 70_Poetry/csharp/Poem.cs | 47 ++++---------------- 4 files changed, 86 insertions(+), 47 deletions(-) delete mode 100644 70_Poetry/csharp/IOExtensions.cs diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs index 0036b52a..9d340c8e 100644 --- a/70_Poetry/csharp/Context.cs +++ b/70_Poetry/csharp/Context.cs @@ -2,10 +2,86 @@ namespace Poetry; internal class Context { + private readonly IReadWrite _io; + private readonly IRandom _random; + + public Context(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + } + public int I { get; set; } public int J { get; set; } public int K { get; set; } public int U { get; set; } public bool SkipComma { get; set; } public bool UseGroup2 { get; set; } + public bool ShouldIndent => U == 0 && J % 2 == 0; + public bool GroupNumberIsValid => J < 5; + + public void WritePhrase() + { + Phrase.GetPhrase(this).Write(_io, this); + } + + public void MaybeWriteComma() + { + if (!SkipComma && _random.NextFloat() <= 0.19F && U != 0) + { + _io.Write(","); + U = 2; + } + SkipComma = false; + } + + public void WriteSpaceOrNewLine() + { + if (_random.NextFloat() <= 0.65F) + { + _io.Write(" "); + U += 1; + } + else + { + _io.WriteLine(); + U = 0; + } + } + + public void Update(IRandom random) + { + I = random.Next(1, 6); + J += 1; + K += 1; + } + + public void MaybeIndent() + { + if (U == 0 && J % 2 == 0) + { + _io.Write(" "); + } + } + + public void ResetGroup(IReadWrite io) + { + J = 0; + io.WriteLine(); + } + + public bool MaybeCompleteStanza() + { + if (K > 20) + { + _io.WriteLine(); + U = K = 0; + UseGroup2 = true; + return true; + } + + return false; + } + + public void SkipNextComma() => SkipComma = true; } diff --git a/70_Poetry/csharp/IOExtensions.cs b/70_Poetry/csharp/IOExtensions.cs deleted file mode 100644 index 631c80f8..00000000 --- a/70_Poetry/csharp/IOExtensions.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Poetry; - -internal static class IOExtensions -{ - internal static void WritePhrase(this IReadWrite io, Context context) - => Phrase.GetPhrase(context).Write(io, context); -} diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs index 3f00abae..919e8a71 100644 --- a/70_Poetry/csharp/Phrase.cs +++ b/70_Poetry/csharp/Phrase.cs @@ -16,7 +16,7 @@ internal class Phrase { new("beguiling me", ctx => ctx.U = 2), new("thrilled me"), - new("still sitting....", ctx => ctx.SkipComma = true), + new("still sitting....", ctx => ctx.SkipNextComma()), new("never flitting", ctx => ctx.U = 2), new("burned") }, @@ -73,7 +73,6 @@ internal class Phrase private static Phrase[] GetGroup(int groupNumber) => _phrases[Math.Max(groupNumber - 1, 0)]; - public void Write(IReadWrite io, Context context) { if (_condition.Invoke(context)) diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs index 64038963..03f3af68 100644 --- a/70_Poetry/csharp/Poem.cs +++ b/70_Poetry/csharp/Poem.cs @@ -8,53 +8,24 @@ internal class Poem { io.Write(Streams.Title); - var context = new Context(); + var context = new Context(io, random); while (true) { - io.WritePhrase(context); - - if (!context.SkipComma && random.NextFloat() <= 0.19F && context.U != 0) - { - io.Write(","); - context.U = 2; - } - context.SkipComma = false; - - if (random.NextFloat() <= 0.65F) - { - io.Write(" "); - context.U += 1; - } - else - { - io.WriteLine(); - context.U = 0; - } + context.WritePhrase(); + context.MaybeWriteComma(); + context.WriteSpaceOrNewLine(); while (true) { - context.I = random.Next(1, 6); - context.J += 1; - context.K += 1; + context.Update(random); + context.MaybeIndent(); - if (context.U == 0 && context.J % 2 == 0) - { - io.Write(" "); - } + if (context.GroupNumberIsValid) { break; } - if (context.J < 5) { break; } + context.ResetGroup(io); - context.J = 0; - io.WriteLine(); - - if (context.K > 20) - { - io.WriteLine(); - context.U = context.K = 0; - context.UseGroup2 = true; - break; - } + if (context.MaybeCompleteStanza()) { break; } } } } From d8dd694ea49acb7d3743a1af0b9b20ca6da58c20 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Mon, 3 Oct 2022 22:19:51 +1100 Subject: [PATCH 05/21] Rationalise visibility of context values --- 70_Poetry/csharp/Context.cs | 55 ++++++++++++++++++++++--------------- 70_Poetry/csharp/Phrase.cs | 17 ++++-------- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs index 9d340c8e..950c7c53 100644 --- a/70_Poetry/csharp/Context.cs +++ b/70_Poetry/csharp/Context.cs @@ -4,6 +4,11 @@ internal class Context { private readonly IReadWrite _io; private readonly IRandom _random; + private int _phraseNumber; + private int _groupNumber; + private bool _skipComma; + private int _lineCount; + private bool _useGroup2; public Context(IReadWrite io, IRandom random) { @@ -11,14 +16,20 @@ internal class Context _random = random; } - public int I { get; set; } - public int J { get; set; } - public int K { get; set; } - public int U { get; set; } - public bool SkipComma { get; set; } - public bool UseGroup2 { get; set; } - public bool ShouldIndent => U == 0 && J % 2 == 0; - public bool GroupNumberIsValid => J < 5; + public int PhraseNumber => Math.Max(_phraseNumber - 1, 0); + + public int GroupNumber + { + get + { + var value = _useGroup2 ? 2 : _groupNumber; + _useGroup2 = false; + return Math.Max(value - 1, 0); + } + } + + public int PhraseCount { get; set; } + public bool GroupNumberIsValid => _groupNumber < 5; public void WritePhrase() { @@ -27,12 +38,12 @@ internal class Context public void MaybeWriteComma() { - if (!SkipComma && _random.NextFloat() <= 0.19F && U != 0) + if (!_skipComma && _random.NextFloat() <= 0.19F && PhraseCount != 0) { _io.Write(","); - U = 2; + PhraseCount = 2; } - SkipComma = false; + _skipComma = false; } public void WriteSpaceOrNewLine() @@ -40,25 +51,25 @@ internal class Context if (_random.NextFloat() <= 0.65F) { _io.Write(" "); - U += 1; + PhraseCount += 1; } else { _io.WriteLine(); - U = 0; + PhraseCount = 0; } } public void Update(IRandom random) { - I = random.Next(1, 6); - J += 1; - K += 1; + _phraseNumber = random.Next(1, 6); + _groupNumber += 1; + _lineCount += 1; } public void MaybeIndent() { - if (U == 0 && J % 2 == 0) + if (PhraseCount == 0 && _groupNumber % 2 == 0) { _io.Write(" "); } @@ -66,22 +77,22 @@ internal class Context public void ResetGroup(IReadWrite io) { - J = 0; + _groupNumber = 0; io.WriteLine(); } public bool MaybeCompleteStanza() { - if (K > 20) + if (_lineCount > 20) { _io.WriteLine(); - U = K = 0; - UseGroup2 = true; + PhraseCount = _lineCount = 0; + _useGroup2 = true; return true; } return false; } - public void SkipNextComma() => SkipComma = true; + public void SkipNextComma() => _skipComma = true; } diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs index 919e8a71..c1ac0866 100644 --- a/70_Poetry/csharp/Phrase.cs +++ b/70_Poetry/csharp/Phrase.cs @@ -2,7 +2,7 @@ namespace Poetry; internal class Phrase { - private static Phrase[][] _phrases = new Phrase[][] + private readonly static Phrase[][] _phrases = new Phrase[][] { new Phrase[] { @@ -14,10 +14,10 @@ internal class Phrase }, new Phrase[] { - new("beguiling me", ctx => ctx.U = 2), + new("beguiling me", ctx => ctx.PhraseCount = 2), new("thrilled me"), new("still sitting....", ctx => ctx.SkipNextComma()), - new("never flitting", ctx => ctx.U = 2), + new("never flitting", ctx => ctx.PhraseCount = 2), new("burned") }, new Phrase[] @@ -26,7 +26,7 @@ internal class Phrase new("darkness there"), new("shall be lifted"), new("quoth the raven"), - new(ctx => ctx.U != 0, "sign of parting") + new(ctx => ctx.PhraseCount != 0, "sign of parting") }, new Phrase[] { @@ -64,14 +64,7 @@ internal class Phrase _update = update; } - public static Phrase GetPhrase(Context context) - { - var group = GetGroup(context.UseGroup2 ? 2 : context.J); - context.UseGroup2 = false; - return group[Math.Max(context.I - 1, 0)]; - } - - private static Phrase[] GetGroup(int groupNumber) => _phrases[Math.Max(groupNumber - 1, 0)]; + public static Phrase GetPhrase(Context context) => _phrases[context.GroupNumber][context.PhraseNumber]; public void Write(IReadWrite io, Context context) { From 739496dd68d68c6dd123c4a2bc542b254606f847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=9Fur=20K=C3=BCpeli?= Date: Tue, 4 Oct 2022 21:59:02 +0300 Subject: [PATCH 06/21] rust implementation --- 21_Calendar/rust/Cargo.toml | 8 ++ 21_Calendar/rust/README.md | 3 + 21_Calendar/rust/src/main.rs | 152 +++++++++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 21_Calendar/rust/Cargo.toml create mode 100644 21_Calendar/rust/README.md create mode 100644 21_Calendar/rust/src/main.rs diff --git a/21_Calendar/rust/Cargo.toml b/21_Calendar/rust/Cargo.toml new file mode 100644 index 00000000..1ec69633 --- /dev/null +++ b/21_Calendar/rust/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/21_Calendar/rust/README.md b/21_Calendar/rust/README.md new file mode 100644 index 00000000..e616424e --- /dev/null +++ b/21_Calendar/rust/README.md @@ -0,0 +1,3 @@ +Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) + +Conversion to [Rust](https://www.rust-lang.org/) by [Uğur Küpeli](https://github.com/ugurkupeli) \ No newline at end of file diff --git a/21_Calendar/rust/src/main.rs b/21_Calendar/rust/src/main.rs new file mode 100644 index 00000000..a279f95d --- /dev/null +++ b/21_Calendar/rust/src/main.rs @@ -0,0 +1,152 @@ +use std::io::stdin; + +const WIDTH: usize = 64; +const DAYS_WIDTH: usize = WIDTH / 8; +const MONTH_WIDTH: usize = WIDTH - (DAYS_WIDTH * 2); +const DAY_NUMS_WIDTH: usize = WIDTH / 7; + +const DAYS: [&str; 7] = [ + "SUNDAY", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", +]; + +fn main() { + println!("\n\t\t CALENDAR"); + println!("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n"); + + let (starting_day, leap_year) = prompt(); + let (months, total_days) = get_months_and_days(leap_year); + + let mut days_passed = 0; + let mut current_day_index = DAYS.iter().position(|d| *d == starting_day).unwrap(); + + for (month, days) in months { + print_header(month, days_passed, total_days - days_passed); + print_days(&mut current_day_index, days); + days_passed += days as u16; + println!("\n"); + } +} + +fn prompt() -> (String, bool) { + let mut day = String::new(); + + loop { + println!("\nFirst day of the year?"); + if let Ok(_) = stdin().read_line(&mut day) { + day = day.trim().to_uppercase(); + if DAYS.contains(&day.as_str()) { + break; + } else { + day.clear(); + } + } + } + + let mut leap = false; + + loop { + println!("Is this a leap year?"); + let mut input = String::new(); + if let Ok(_) = stdin().read_line(&mut input) { + match input.to_uppercase().trim() { + "Y" | "YES" => { + leap = true; + break; + } + "N" | "NO" => break, + _ => (), + } + } + } + + println!(); + (day, leap) +} + +fn get_months_and_days(leap_year: bool) -> (Vec<(String, u8)>, u16) { + let months = [ + "JANUARY", + "FEBUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER", + ]; + + let mut months_with_days = Vec::new(); + let mut total_days: u16 = 0; + + for (i, month) in months.iter().enumerate() { + let days = if i == 1 { + if leap_year { + 29u8 + } else { + 28 + } + } else if if i < 7 { (i % 2) == 0 } else { (i % 2) != 0 } { + 31 + } else { + 30 + }; + + total_days += days as u16; + months_with_days.push((month.to_string(), days)); + } + + (months_with_days, total_days) +} + +fn print_between(s: String, w: usize, star: bool) { + let s = format!(" {s} "); + if star { + print!("{:*^w$}", s); + return; + } + print!("{:^w$}", s); +} + +fn print_header(month: String, days_passed: u16, days_left: u16) { + print_between(days_passed.to_string(), DAYS_WIDTH, true); + print_between(month.to_string(), MONTH_WIDTH, true); + print_between(days_left.to_string(), DAYS_WIDTH, true); + println!(); + + for d in DAYS { + let d = d.chars().nth(0).unwrap(); + print_between(d.to_string(), DAY_NUMS_WIDTH, false); + } + println!(); + + println!("{:*>WIDTH$}", ""); +} + +fn print_days(current_day_index: &mut usize, days: u8) { + let mut current_date = 1u8; + + print!("{:>w$}", " ", w = DAY_NUMS_WIDTH * *current_day_index); + + for _ in 1..=days { + print_between(current_date.to_string(), DAY_NUMS_WIDTH, false); + + if ((*current_day_index + 1) % 7) == 0 { + *current_day_index = 0; + println!(); + } else { + *current_day_index += 1; + } + + current_date += 1; + } +} From 257ba1ab1799d56936f4187824703fb0a414ace4 Mon Sep 17 00:00:00 2001 From: Andrew Cooper Date: Wed, 5 Oct 2022 07:45:50 +1100 Subject: [PATCH 07/21] Capitalise first char of line --- 70_Poetry/csharp/Context.cs | 17 ++++++++++++++--- 70_Poetry/csharp/Phrase.cs | 2 +- 70_Poetry/csharp/Poem.cs | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs index 950c7c53..a9297915 100644 --- a/70_Poetry/csharp/Context.cs +++ b/70_Poetry/csharp/Context.cs @@ -9,6 +9,7 @@ internal class Context private bool _skipComma; private int _lineCount; private bool _useGroup2; + private bool _atStartOfLine = true; public Context(IReadWrite io, IRandom random) { @@ -34,6 +35,7 @@ internal class Context public void WritePhrase() { Phrase.GetPhrase(this).Write(_io, this); + _atStartOfLine = false; } public void MaybeWriteComma() @@ -55,7 +57,7 @@ internal class Context } else { - _io.WriteLine(); + EndLine(); PhraseCount = 0; } } @@ -75,10 +77,10 @@ internal class Context } } - public void ResetGroup(IReadWrite io) + public void ResetGroup() { _groupNumber = 0; - io.WriteLine(); + EndLine(); } public bool MaybeCompleteStanza() @@ -94,5 +96,14 @@ internal class Context return false; } + internal string MaybeCapitalise(string text) => + _atStartOfLine ? (char.ToUpper(text[0]) + text[1..]) : text; + public void SkipNextComma() => _skipComma = true; + + public void EndLine() + { + _io.WriteLine(); + _atStartOfLine = true; + } } diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs index c1ac0866..f70de30b 100644 --- a/70_Poetry/csharp/Phrase.cs +++ b/70_Poetry/csharp/Phrase.cs @@ -70,7 +70,7 @@ internal class Phrase { if (_condition.Invoke(context)) { - io.Write(_text); + io.Write(context.MaybeCapitalise(_text)); } _update.Invoke(context); diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs index 03f3af68..fa3d5045 100644 --- a/70_Poetry/csharp/Poem.cs +++ b/70_Poetry/csharp/Poem.cs @@ -23,7 +23,7 @@ internal class Poem if (context.GroupNumberIsValid) { break; } - context.ResetGroup(io); + context.ResetGroup(); if (context.MaybeCompleteStanza()) { break; } } From ab73a8b75c0b9653db3505553a2152df0e643c10 Mon Sep 17 00:00:00 2001 From: Anthony Rubick <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Tue, 4 Oct 2022 18:13:42 -0700 Subject: [PATCH 08/21] Update README.md --- 24_Chemist/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/24_Chemist/README.md b/24_Chemist/README.md index b36f0b04..fc62425a 100644 --- a/24_Chemist/README.md +++ b/24_Chemist/README.md @@ -17,5 +17,7 @@ http://www.vintage-basic.net/games.html (please note any difficulties or challenges in porting here) +There is a type in the original Basic, "...DECIDE **WHO** MUCH WATER..." should be "DECIDE **HOW** MUCH WATER" + #### External Links - C: https://github.com/ericfischer/basic-computer-games/blob/main/24%20Chemist/c/chemist.c From 45df4253487f9c3f735006d8fc6a96473307401f Mon Sep 17 00:00:00 2001 From: aconconi Date: Thu, 6 Oct 2022 16:10:27 +0200 Subject: [PATCH 09/21] Lua port and readme for 29_Craps added --- 29_Craps/lua/README.md | 16 ++++- 29_Craps/lua/craps.lua | 141 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 29_Craps/lua/craps.lua diff --git a/29_Craps/lua/README.md b/29_Craps/lua/README.md index c063f42f..091ddb4c 100644 --- a/29_Craps/lua/README.md +++ b/29_Craps/lua/README.md @@ -1,3 +1,17 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) -Conversion to [Lua](https://www.lua.org/) +Conversion to [Lua](https://www.lua.org/) by Alex Conconi + +--- + +#### Lua porting notes + +- The `craps_main` function contains the main game loop, which iteratively +plays craps rounds by calling `play_round` and tracks winnings and losings. +- Replaced the original routine that tries to scramble the random number +generator with a proper seed initializer in Lua: `math.randomseed(os.time())` +(as advised in the general porting notes). += Added basic input validation to accept only positive integers for the +wager and the answer to the "If you want to play again print 5" question. +- "If you want to play again print 5 if not print 2" reads a bit odd but +we decided to leave it as is and stay true to the BASIC original version. \ No newline at end of file diff --git a/29_Craps/lua/craps.lua b/29_Craps/lua/craps.lua new file mode 100644 index 00000000..cdb9c6e3 --- /dev/null +++ b/29_Craps/lua/craps.lua @@ -0,0 +1,141 @@ +--[[ +Craps + +From: BASIC Computer Games (1978) +Edited by David H. Ahl + + This game simulates the games of craps played according to standard + Nevada craps table rules. That is: + 1. A 7 or 11 on the first roll wins + 2. A 2, 3, or 12 on the first roll loses + 3. Any other number rolled becomes your "point." You continue to roll; + if you get your point you win. If you roll a 7, you lose and the dice + change hands when this happens. + + This version of craps was modified by Steve North of Creative Computing. + It is based on an original which appeared one day one a computer at DEC. + + +Lua port by Alex Conconi, 2022 +--]] + + +--- Throw two dice and return their sum. +local function throw_dice() + return math.random(1, 6) + math.random(1, 6) +end + + +--- Print prompt and read a number > 0 from stdin. +local function input_number(prompt) + while true do + io.write(prompt) + local number = tonumber(io.stdin:read("*l")) + if number and number > 0 then + return number + else + print("Please enter a number greater than zero.") + end + end +end + + +--- Play a round and return winnings or losings. +local function play_round() + -- Input the wager + local wager = input_number("Input the amount of your wager: ") + + -- Roll the die for the first time. + print("I will now throw the dice") + local first_roll = throw_dice() + + -- A 7 or 11 on the first roll wins. + if first_roll == 7 or first_roll == 11 then + print(string.format("%d - natural.... a winner!!!!", first_roll)) + print(string.format("%d pays even money, you win %d dollars", first_roll, wager)) + return wager + end + + -- A 2, 3, or 12 on the first roll loses. + if first_roll == 2 or first_roll == 3 or first_roll == 12 then + if first_roll == 2 then + -- Special 'you lose' message for 'snake eyes' + print(string.format("%d - snake eyes.... you lose.", first_roll)) + else + -- Default 'you lose' message + print(string.format("%d - craps.... you lose.", first_roll)) + end + print(string.format("You lose %d dollars", wager)) + return -wager + end + + -- Any other number rolled becomes your "point." You continue to roll; + -- if you get your point you win. If you roll a 7, you lose and the dice + -- change hands when this happens. + print(string.format("%d is the point. I will roll again", first_roll)) + local second_roll + repeat + second_roll = throw_dice() + if second_roll == first_roll then + -- Player gets point and wins + print(string.format("%d - a winner.........congrats!!!!!!!!", first_roll)) + print(string.format("%d at 2 to 1 odds pays you...let me see... %d dollars", first_roll, 2 * wager)) + return 2 * wager + end + if second_roll == 7 then + -- Player gets 7 and loses + print(string.format("%d - craps. You lose.", second_roll)) + print(string.format("You lose $ %d", wager)) + return -wager + end + -- Continue to roll + print(string.format("%d - no point. I will roll again", second_roll)) + until second_roll == first_roll or second_roll == 7 +end + + +--- Main game function. +local function craps_main() + -- Print the introduction to the game + print(string.rep(" ", 32) .. "Craps") + print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n") + print("2,3,12 are losers; 4,5,6,8,9,10 are points; 7,11 are natural winners.") + + -- Initialize random number generator seeed + math.randomseed(os.time()) + + -- Initialize balance to track winnings and losings + local balance = 0 + + -- Main game loop + local keep_playing = true + while keep_playing do + -- Play a round + balance = balance + play_round() + + -- If player's answer is 5, then stop playing + keep_playing = (input_number("If you want to play again print 5 if not print 2: ") == 5) + + -- Print an update on money won + if balance < 0 then + print(string.format("You are now under $%d", -balance)) + elseif balance > 0 then + print(string.format("You are now ahead $%d", balance)) + else + print("You are now even at 0") + end + end + + -- Game over, print the goodbye message + if balance < 0 then + print("Too bad, you are in the hole. Come again.") + elseif balance > 0 then + print("Congratulations---you came out a winner. Come again.") + else + print("Congratulations---you came out even, not bad for an amateur") + end +end + + +--- Run the game. +craps_main() From 4dcde245f6b6391a2eed92b9f5dcfea8e8da049d Mon Sep 17 00:00:00 2001 From: aconconi Date: Fri, 7 Oct 2022 18:59:43 +0200 Subject: [PATCH 10/21] added print_balance function, linting --- 29_Craps/lua/craps.lua | 66 +++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/29_Craps/lua/craps.lua b/29_Craps/lua/craps.lua index cdb9c6e3..063adad8 100644 --- a/29_Craps/lua/craps.lua +++ b/29_Craps/lua/craps.lua @@ -40,10 +40,22 @@ local function input_number(prompt) end +--- Print custom balance message depending on balance value +local function print_balance(balance, under_msg, ahead_msg, even_msg) + if balance < 0 then + print(under_msg) + elseif balance > 0 then + print(ahead_msg) + else + print(even_msg) + end +end + + --- Play a round and return winnings or losings. local function play_round() - -- Input the wager - local wager = input_number("Input the amount of your wager: ") + -- Input the wager + local wager = input_number("Input the amount of your wager: ") -- Roll the die for the first time. print("I will now throw the dice") @@ -69,13 +81,11 @@ local function play_round() return -wager end - -- Any other number rolled becomes your "point." You continue to roll; - -- if you get your point you win. If you roll a 7, you lose and the dice - -- change hands when this happens. + -- Any other number rolled becomes the "point". + -- Continue to roll until rolling a 7 or point. print(string.format("%d is the point. I will roll again", first_roll)) - local second_roll - repeat - second_roll = throw_dice() + while true do + local second_roll = throw_dice() if second_roll == first_roll then -- Player gets point and wins print(string.format("%d - a winner.........congrats!!!!!!!!", first_roll)) @@ -83,14 +93,14 @@ local function play_round() return 2 * wager end if second_roll == 7 then - -- Player gets 7 and loses + -- Player rolls a 7 and loses print(string.format("%d - craps. You lose.", second_roll)) print(string.format("You lose $ %d", wager)) return -wager end -- Continue to roll print(string.format("%d - no point. I will roll again", second_roll)) - until second_roll == first_roll or second_roll == 7 + end end @@ -98,17 +108,17 @@ end local function craps_main() -- Print the introduction to the game print(string.rep(" ", 32) .. "Craps") - print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n") + print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n") print("2,3,12 are losers; 4,5,6,8,9,10 are points; 7,11 are natural winners.") - -- Initialize random number generator seeed + -- Initialize random number generator seed math.randomseed(os.time()) - -- Initialize balance to track winnings and losings + -- Initialize balance to track winnings and losings local balance = 0 -- Main game loop - local keep_playing = true + local keep_playing = true while keep_playing do -- Play a round balance = balance + play_round() @@ -116,24 +126,22 @@ local function craps_main() -- If player's answer is 5, then stop playing keep_playing = (input_number("If you want to play again print 5 if not print 2: ") == 5) - -- Print an update on money won - if balance < 0 then - print(string.format("You are now under $%d", -balance)) - elseif balance > 0 then - print(string.format("You are now ahead $%d", balance)) - else - print("You are now even at 0") - end + -- Print an update on winnings or losings + print_balance( + balance, + string.format("You are now under $%d", -balance), + string.format("You are now ahead $%d", balance), + "You are now even at 0" + ) end -- Game over, print the goodbye message - if balance < 0 then - print("Too bad, you are in the hole. Come again.") - elseif balance > 0 then - print("Congratulations---you came out a winner. Come again.") - else - print("Congratulations---you came out even, not bad for an amateur") - end + print_balance( + balance, + "Too bad, you are in the hole. Come again.", + "Congratulations---you came out a winner. Come again.", + "Congratulations---you came out even, not bad for an amateur" + ) end From 3a042277cd649fccb7829d86274313872f3efa11 Mon Sep 17 00:00:00 2001 From: AnthonyMichaelTDM <68485672+AnthonyMichaelTDM@users.noreply.github.com> Date: Fri, 7 Oct 2022 16:41:07 -0700 Subject: [PATCH 11/21] update markdown_todo_rust --- 00_Utilities/markdown_todo_rust/src/main.rs | 23 ++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/00_Utilities/markdown_todo_rust/src/main.rs b/00_Utilities/markdown_todo_rust/src/main.rs index d1f49bf4..9df64567 100644 --- a/00_Utilities/markdown_todo_rust/src/main.rs +++ b/00_Utilities/markdown_todo_rust/src/main.rs @@ -1,4 +1,4 @@ -use std::ffi::{OsString, OsStr}; +use std::ffi::OsStr; use std::{fs, io}; use std::fs::metadata; use std::path::{Path, PathBuf}; @@ -8,8 +8,6 @@ use std::path::{Path, PathBuf}; * @author Anthony Rubick */ - - //DATA const ROOT_DIR: &str = "../../"; const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language name, second element is the file extension @@ -25,13 +23,19 @@ const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language ("vbnet", "vb") ]; const OUTPUT_PATH: &str = "../../todo.md"; -//const INGORE: [&str;5] = ["../../.git","../../.vscode","../../00_Utilities","../../buildJvm","../../node_modules"]; //folders to ignore fn main() { //DATA let mut root_folders:Vec; let mut output_string: String = String::new(); let format_game_first: bool; + let ingore: [PathBuf;5] = [ + PathBuf::from(r"../../.git"), + PathBuf::from(r"../../.github"), + PathBuf::from(r"../../00_Alternate_Languages"), + PathBuf::from(r"../../00_Utilities"), + PathBuf::from(r"../../00_Common"), + ]; //folders to ignore //print welcome message println!(" @@ -66,15 +70,10 @@ fn main() { //for all folders, search for the languages and extensions root_folders = root_folders.into_iter().filter(|path| { - match fs::read_dir(path) { - Err(why) => {println!("! {:?}", why.kind()); false}, - Ok(paths) => { - paths.into_iter().filter(|f| metadata(f.as_ref().unwrap().path()).unwrap().is_dir()) //filter to only folders - .filter_map( |path| path.ok() ) //extract only the DirEntries - .any(|f| LANGUAGES.iter().any(|tup| OsString::from(tup.1).eq_ignore_ascii_case(f.file_name()))) //filter out ones that don't contain folders with the language names - } - } + //not one of the ignored folders + !ingore.contains(path) }).collect(); + root_folders.sort(); //create todo list if format_game_first { From 3bf29b6f123a3d38047000ad1855be7ec5d91869 Mon Sep 17 00:00:00 2001 From: aconconi Date: Sat, 8 Oct 2022 13:08:17 +0200 Subject: [PATCH 12/21] minor edits --- 29_Craps/lua/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/29_Craps/lua/README.md b/29_Craps/lua/README.md index 091ddb4c..aac11dd0 100644 --- a/29_Craps/lua/README.md +++ b/29_Craps/lua/README.md @@ -4,14 +4,14 @@ Conversion to [Lua](https://www.lua.org/) by Alex Conconi --- -#### Lua porting notes +### Lua porting notes - The `craps_main` function contains the main game loop, which iteratively plays craps rounds by calling `play_round` and tracks winnings and losings. - Replaced the original routine that tries to scramble the random number generator with a proper seed initializer in Lua: `math.randomseed(os.time())` (as advised in the general porting notes). -= Added basic input validation to accept only positive integers for the +- Added basic input validation to accept only positive integers for the wager and the answer to the "If you want to play again print 5" question. - "If you want to play again print 5 if not print 2" reads a bit odd but we decided to leave it as is and stay true to the BASIC original version. \ No newline at end of file From 97b28ee5b44ca2d97c3fdca845e9a82998ea63b0 Mon Sep 17 00:00:00 2001 From: aconconi Date: Sat, 8 Oct 2022 20:52:11 +0200 Subject: [PATCH 13/21] converted indentation to spaces --- 29_Craps/lua/craps.lua | 178 ++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/29_Craps/lua/craps.lua b/29_Craps/lua/craps.lua index 063adad8..ddd1ce30 100644 --- a/29_Craps/lua/craps.lua +++ b/29_Craps/lua/craps.lua @@ -22,126 +22,126 @@ Lua port by Alex Conconi, 2022 --- Throw two dice and return their sum. local function throw_dice() - return math.random(1, 6) + math.random(1, 6) + return math.random(1, 6) + math.random(1, 6) end --- Print prompt and read a number > 0 from stdin. local function input_number(prompt) - while true do - io.write(prompt) - local number = tonumber(io.stdin:read("*l")) - if number and number > 0 then - return number - else - print("Please enter a number greater than zero.") - end - end + while true do + io.write(prompt) + local number = tonumber(io.stdin:read("*l")) + if number and number > 0 then + return number + else + print("Please enter a number greater than zero.") + end + end end --- Print custom balance message depending on balance value local function print_balance(balance, under_msg, ahead_msg, even_msg) - if balance < 0 then - print(under_msg) - elseif balance > 0 then - print(ahead_msg) - else - print(even_msg) - end + if balance < 0 then + print(under_msg) + elseif balance > 0 then + print(ahead_msg) + else + print(even_msg) + end end --- Play a round and return winnings or losings. local function play_round() - -- Input the wager - local wager = input_number("Input the amount of your wager: ") + -- Input the wager + local wager = input_number("Input the amount of your wager: ") - -- Roll the die for the first time. - print("I will now throw the dice") - local first_roll = throw_dice() + -- Roll the die for the first time. + print("I will now throw the dice") + local first_roll = throw_dice() - -- A 7 or 11 on the first roll wins. - if first_roll == 7 or first_roll == 11 then - print(string.format("%d - natural.... a winner!!!!", first_roll)) - print(string.format("%d pays even money, you win %d dollars", first_roll, wager)) - return wager - end + -- A 7 or 11 on the first roll wins. + if first_roll == 7 or first_roll == 11 then + print(string.format("%d - natural.... a winner!!!!", first_roll)) + print(string.format("%d pays even money, you win %d dollars", first_roll, wager)) + return wager + end - -- A 2, 3, or 12 on the first roll loses. - if first_roll == 2 or first_roll == 3 or first_roll == 12 then - if first_roll == 2 then - -- Special 'you lose' message for 'snake eyes' - print(string.format("%d - snake eyes.... you lose.", first_roll)) - else - -- Default 'you lose' message - print(string.format("%d - craps.... you lose.", first_roll)) - end - print(string.format("You lose %d dollars", wager)) - return -wager - end + -- A 2, 3, or 12 on the first roll loses. + if first_roll == 2 or first_roll == 3 or first_roll == 12 then + if first_roll == 2 then + -- Special 'you lose' message for 'snake eyes' + print(string.format("%d - snake eyes.... you lose.", first_roll)) + else + -- Default 'you lose' message + print(string.format("%d - craps.... you lose.", first_roll)) + end + print(string.format("You lose %d dollars", wager)) + return -wager + end - -- Any other number rolled becomes the "point". + -- Any other number rolled becomes the "point". -- Continue to roll until rolling a 7 or point. - print(string.format("%d is the point. I will roll again", first_roll)) - while true do - local second_roll = throw_dice() - if second_roll == first_roll then - -- Player gets point and wins - print(string.format("%d - a winner.........congrats!!!!!!!!", first_roll)) - print(string.format("%d at 2 to 1 odds pays you...let me see... %d dollars", first_roll, 2 * wager)) - return 2 * wager - end - if second_roll == 7 then - -- Player rolls a 7 and loses - print(string.format("%d - craps. You lose.", second_roll)) - print(string.format("You lose $ %d", wager)) - return -wager - end - -- Continue to roll - print(string.format("%d - no point. I will roll again", second_roll)) - end + print(string.format("%d is the point. I will roll again", first_roll)) + while true do + local second_roll = throw_dice() + if second_roll == first_roll then + -- Player gets point and wins + print(string.format("%d - a winner.........congrats!!!!!!!!", first_roll)) + print(string.format("%d at 2 to 1 odds pays you...let me see... %d dollars", first_roll, 2 * wager)) + return 2 * wager + end + if second_roll == 7 then + -- Player rolls a 7 and loses + print(string.format("%d - craps. You lose.", second_roll)) + print(string.format("You lose $ %d", wager)) + return -wager + end + -- Continue to roll + print(string.format("%d - no point. I will roll again", second_roll)) + end end --- Main game function. local function craps_main() - -- Print the introduction to the game - print(string.rep(" ", 32) .. "Craps") - print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n") - print("2,3,12 are losers; 4,5,6,8,9,10 are points; 7,11 are natural winners.") + -- Print the introduction to the game + print(string.rep(" ", 32) .. "Craps") + print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n") + print("2,3,12 are losers; 4,5,6,8,9,10 are points; 7,11 are natural winners.") - -- Initialize random number generator seed - math.randomseed(os.time()) + -- Initialize random number generator seed + math.randomseed(os.time()) - -- Initialize balance to track winnings and losings - local balance = 0 + -- Initialize balance to track winnings and losings + local balance = 0 - -- Main game loop - local keep_playing = true - while keep_playing do - -- Play a round - balance = balance + play_round() + -- Main game loop + local keep_playing = true + while keep_playing do + -- Play a round + balance = balance + play_round() - -- If player's answer is 5, then stop playing - keep_playing = (input_number("If you want to play again print 5 if not print 2: ") == 5) + -- If player's answer is 5, then stop playing + keep_playing = (input_number("If you want to play again print 5 if not print 2: ") == 5) - -- Print an update on winnings or losings - print_balance( - balance, - string.format("You are now under $%d", -balance), - string.format("You are now ahead $%d", balance), - "You are now even at 0" - ) - end + -- Print an update on winnings or losings + print_balance( + balance, + string.format("You are now under $%d", -balance), + string.format("You are now ahead $%d", balance), + "You are now even at 0" + ) + end - -- Game over, print the goodbye message - print_balance( - balance, - "Too bad, you are in the hole. Come again.", - "Congratulations---you came out a winner. Come again.", - "Congratulations---you came out even, not bad for an amateur" - ) + -- Game over, print the goodbye message + print_balance( + balance, + "Too bad, you are in the hole. Come again.", + "Congratulations---you came out a winner. Come again.", + "Congratulations---you came out even, not bad for an amateur" + ) end From 0d2b7c655974380fc143c030372313e03494f655 Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 12:43:22 -0400 Subject: [PATCH 14/21] Initial commit of Life in Rust --- 55_Life/rust/Cargo.toml | 8 ++ 55_Life/rust/src/main.rs | 253 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 55_Life/rust/Cargo.toml create mode 100644 55_Life/rust/src/main.rs diff --git a/55_Life/rust/Cargo.toml b/55_Life/rust/Cargo.toml new file mode 100644 index 00000000..1ec69633 --- /dev/null +++ b/55_Life/rust/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs new file mode 100644 index 00000000..d2901e6c --- /dev/null +++ b/55_Life/rust/src/main.rs @@ -0,0 +1,253 @@ +use std::{io, thread, time}; + +const HEIGHT:usize = 24; +const WIDTH:usize = 70; + +// The BASIC implementation uses a 24x70 array of integers to represent the board state. +// 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. +// (I'm not actually sure whether there are other values besides zero.) +// Here, we'll use an enum instead. +#[derive(Clone, Copy, PartialEq)] +enum CellState { + Empty, + Alive, + AboutToDie, + AboutToBeBorn +} + +// Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. +// Since that isn't too big (even in the 70's), we just store the whole board as an +// array of CellState. I'm experimenting with using an array-of-arrays to make references +// more convenient. +struct Board { + cells: [[CellState; WIDTH]; HEIGHT], + min_row: usize, + max_row: usize, + min_col: usize, + max_col: usize, + population: usize, + generation: usize, + invalid: bool +} + +impl Board { + fn new() -> Board { + Board { + cells: [[CellState::Empty; WIDTH]; HEIGHT], + min_row: 0, + max_row: 0, + min_col: 0, + max_col: 0, + population: 0, + generation: 1, + invalid: false, + } + } +} + +fn main() { + println!(); println!(); println!(); + println!("Enter your pattern: "); + let mut board = parse_pattern(get_pattern()); + loop { + finish_cell_transitions(&mut board); + print_board(&board); + update_bounds(&mut board); + update_board(&mut board); + if board.population == 0 { + break; // this isn't in the original implementation but I wanted it + } + delay(); + } +} + +fn get_pattern() -> Vec { + let mut lines = Vec::new(); + loop { + let mut line = String::new(); + // read_line reads into the buffer (appending if it's not empty). + // It returns the number of characters read, including the newline. This will be 0 on EOF. + // unwrap() will panic and terminate the program if there is an error reading from stdin. + // I think that's reasonable behavior in this case. + let nread = io::stdin().read_line(&mut line).unwrap(); + let line = line.trim_end(); + if nread == 0 || line.eq_ignore_ascii_case("DONE") { + return lines; + } + lines.push(line.to_string()); + } +} + +fn parse_pattern(rows: Vec) -> Board { + // A robust program would check the bounds of the inputs here. I'm not doing that, + // because the BASIC implementation didn't, and for me, part of the joy of these + // books back in the day was learning how my inputs could break things. + + let mut board = Board::new(); + + // Strings are UTF-8 in Rust, so characters can take multiple bytes. We will convert + // each to a Vec up front so that we don't have to do that conversion multiple + // times (to find the length of the strings in chars, then to parse each char). + // The into_iter() method consumes rows() so it can no longer be used. + let char_vecs = Vec::from_iter(rows.into_iter().map(|s| Vec::from_iter(s.chars()))); + + // The BASIC implementation puts the pattern roughly in the center of the board, + // assuming that there are no blank rows at the beginning or end, or blanks entered + // at the beginning or end of every row. It wouldn't be hard to check for that, but + // for now we'll preserve the original behavior. + let nrows = char_vecs.len(); + let ncols = char_vecs.iter() + .map(|l| l.len()) + .max() + .unwrap_or(0); // handles the case where rows is empty + + // Note that there's a subtlety here. The len() method returns a usize, i.e., an + // unsigned int, so the result type is the same. If nlines >= 24 or ncols >= 68, the + // result will wrap around to a giant value. These are stricter limits than you'd + // expect from just looking at the 24x70 bounds, but again, we're preserving the + // original behavior. + board.min_row = 11 - nrows / 2; + board.min_col = 33 - ncols / 2; + board.max_row = board.min_row + nrows - 1; + board.max_col = board.min_col + ncols - 1; + + // Loop over the rows provided. The enumerate() method augments the iterator with an index. + for (row_index, pattern) in char_vecs.iter().enumerate() + { + let row = board.min_row + row_index; + // Now loop over the non-empty cells in the current row. filter_map takes a closure that + // returns an Option. If the Option is None, filter_map filters out that entry from the + // for loop. If it's Some(x), filter_map executes the loop body with the value x. + for col in pattern.iter().enumerate().filter_map(|(col_index, chr)| { + if *chr == ' ' || (*chr == '.' && col_index == 0) { + None + } else { + Some(board.min_col + col_index) + }}) + { + board.cells[row][col] = CellState::Alive; + board.population += 1; + } + } + + + board +} + +fn finish_cell_transitions(board: &mut Board) { + for row in board.cells[board.min_row-1..=board.max_row+1].iter_mut() { + for cell in row[board.min_col-1..=board.max_col+1].iter_mut() { + if *cell == CellState::AboutToBeBorn { + *cell = CellState::Alive; + board.population += 1; + } else if *cell == CellState::AboutToDie { + *cell = CellState::Empty; + board.population -= 1; + } + } + } +} + +fn print_board(board: &Board) { + println!(); println!(); println!(); + println!("Generation: {}", board.generation); + println!("Population: {}", board.population); + if board.invalid { + println!("Invalid!"); + } + for row_index in 0..HEIGHT { + for col_index in 0..WIDTH { + let rep = if board.cells[row_index][col_index] == CellState::Alive { "*" } else { " " }; + print!("{rep}"); + } + println!(); + } +} + +fn update_bounds(board: &mut Board) { + // In the BASIC implementation, this happens in the same loop that prints the board. + // We're breaking it out to improve separation of concerns. + // We could improve efficiency here by only searching one row outside the previous bounds. + board.min_row = HEIGHT; + board.max_row = 0; + board.min_col = WIDTH; + board.max_col = 0; + for (irow, row) in board.cells.iter().enumerate() { + let mut any_set = false; + for (icol, cell) in row.iter().enumerate() { + if *cell == CellState::Alive { + any_set = true; + if board.min_col > icol { + board.min_col = icol; + } + if board.max_col < icol { + board.max_col = icol; + } + } + } + if any_set { + if board.min_row > irow { + board.min_row = irow; + } + if board.max_row < irow { + board.max_row = irow; + } + } + } + // If anything is alive within two cells of the boundary, mark the board invalid and + // clamp the bounds. We need a two-cell margin because we'll count neighbors on cells + // one space outside the min/max, and when we count neighbors we go out by an + // additional space. + if board.min_row < 2 { + board.min_row = 2; + board.invalid = true; + } + if board.max_row > HEIGHT - 3 { + board.max_row = HEIGHT - 3; + board.invalid = true; + } + if board.min_col < 2 { + board.min_col = 2; + board.invalid = true; + } + if board.max_col > WIDTH - 3 { + board.max_col = WIDTH - 3; + board.invalid = true; + } +} + +fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { + let mut count = 0; + assert!((1..=HEIGHT-2).contains(&row_index)); + assert!((1..=WIDTH-2).contains(&col_index)); + for i in row_index-1..=row_index+1 { + for j in col_index-1..=col_index+1 { + if i == row_index && j == col_index { + continue; + } + if board.cells[i][j] == CellState::Alive || board.cells [i][j] == CellState::AboutToDie { + count += 1; + } + } + } + count +} + +fn update_board(board: &mut Board) { + for row_index in board.min_row-1..=board.max_row+1 { + for col_index in board.min_col-1..=board.max_col+1 { + let neighbors = count_neighbors(board, row_index, col_index); + let this_cell_state = &mut board.cells[row_index][col_index]; // borrow a mutable reference to the array cell + *this_cell_state = match *this_cell_state { + CellState::Empty if neighbors == 3 => CellState::AboutToBeBorn, + CellState::Alive if !(2..=3).contains(&neighbors) => CellState::AboutToDie, + _ => *this_cell_state + } + } + } + board.generation += 1; +} + +fn delay() { + thread::sleep(time::Duration::from_millis(500)); +} From a068af4bc9670470e5f82f1d9d52c1d2c77ad80c Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 16:26:28 -0400 Subject: [PATCH 15/21] Do bounds update in finish_cell_transitions Merged the functionality of update_bounds into finish_cell_transitions, eliminating a loop. --- 55_Life/rust/src/main.rs | 107 +++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 55 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index d2901e6c..d7ccb4e3 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -52,7 +52,6 @@ fn main() { loop { finish_cell_transitions(&mut board); print_board(&board); - update_bounds(&mut board); update_board(&mut board); if board.population == 0 { break; // this isn't in the original implementation but I wanted it @@ -135,8 +134,16 @@ fn parse_pattern(rows: Vec) -> Board { } fn finish_cell_transitions(board: &mut Board) { - for row in board.cells[board.min_row-1..=board.max_row+1].iter_mut() { - for cell in row[board.min_col-1..=board.max_col+1].iter_mut() { + // In the BASIC implementation, this happens in the same loop that prints the board. + // We're breaking it out to improve separation of concerns. + let mut min_row = HEIGHT - 1; + let mut max_row = 0usize; + let mut min_col = WIDTH - 1; + let mut max_col = 0usize; + for row_index in board.min_row-1..=board.max_row+1 { + let mut any_alive_this_row = false; + for col_index in board.min_col-1..=board.max_col+1 { + let cell = &mut board.cells[row_index][col_index]; if *cell == CellState::AboutToBeBorn { *cell = CellState::Alive; board.population += 1; @@ -144,8 +151,50 @@ fn finish_cell_transitions(board: &mut Board) { *cell = CellState::Empty; board.population -= 1; } + if *cell == CellState::Alive { + any_alive_this_row = true; + if min_col > col_index { + min_col = col_index; + } + if max_col < col_index { + max_col = col_index; + } + } + } + if any_alive_this_row { + if min_row > row_index { + min_row = row_index; + } + if max_row < row_index { + max_row = row_index; + } } } + // If anything is alive within two cells of the boundary, mark the board invalid and + // clamp the bounds. We need a two-cell margin because we'll count neighbors on cells + // one space outside the min/max, and when we count neighbors we go out by an + // additional space. + if min_row < 2 { + min_row = 2; + board.invalid = true; + } + if max_row > HEIGHT - 3 { + max_row = HEIGHT - 3; + board.invalid = true; + } + if min_col < 2 { + min_col = 2; + board.invalid = true; + } + if max_col > WIDTH - 3 { + max_col = WIDTH - 3; + board.invalid = true; + } + + board.min_row = min_row; + board.max_row = max_row; + board.min_col = min_col; + board.max_col = max_col; } fn print_board(board: &Board) { @@ -164,58 +213,6 @@ fn print_board(board: &Board) { } } -fn update_bounds(board: &mut Board) { - // In the BASIC implementation, this happens in the same loop that prints the board. - // We're breaking it out to improve separation of concerns. - // We could improve efficiency here by only searching one row outside the previous bounds. - board.min_row = HEIGHT; - board.max_row = 0; - board.min_col = WIDTH; - board.max_col = 0; - for (irow, row) in board.cells.iter().enumerate() { - let mut any_set = false; - for (icol, cell) in row.iter().enumerate() { - if *cell == CellState::Alive { - any_set = true; - if board.min_col > icol { - board.min_col = icol; - } - if board.max_col < icol { - board.max_col = icol; - } - } - } - if any_set { - if board.min_row > irow { - board.min_row = irow; - } - if board.max_row < irow { - board.max_row = irow; - } - } - } - // If anything is alive within two cells of the boundary, mark the board invalid and - // clamp the bounds. We need a two-cell margin because we'll count neighbors on cells - // one space outside the min/max, and when we count neighbors we go out by an - // additional space. - if board.min_row < 2 { - board.min_row = 2; - board.invalid = true; - } - if board.max_row > HEIGHT - 3 { - board.max_row = HEIGHT - 3; - board.invalid = true; - } - if board.min_col < 2 { - board.min_col = 2; - board.invalid = true; - } - if board.max_col > WIDTH - 3 { - board.max_col = WIDTH - 3; - board.invalid = true; - } -} - fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { let mut count = 0; assert!((1..=HEIGHT-2).contains(&row_index)); From 15724219b5500b903880d56907b7d8746bba298b Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 20:29:40 -0400 Subject: [PATCH 16/21] Input bounds checking, refactor and cleanup get_pattern now checks the size of the input to prevent out of bounds writes., and converts String to Vec immediately. Refactors: changed function names, ran rust-fmt, improved some comments --- 55_Life/rust/src/main.rs | 95 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index d7ccb4e3..37e0f6c0 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,7 +1,7 @@ use std::{io, thread, time}; -const HEIGHT:usize = 24; -const WIDTH:usize = 70; +const HEIGHT: usize = 24; +const WIDTH: usize = 70; // The BASIC implementation uses a 24x70 array of integers to represent the board state. // 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. @@ -12,7 +12,7 @@ enum CellState { Empty, Alive, AboutToDie, - AboutToBeBorn + AboutToBeBorn, } // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. @@ -27,7 +27,7 @@ struct Board { max_col: usize, population: usize, generation: usize, - invalid: bool + invalid: bool, } impl Board { @@ -52,84 +52,86 @@ fn main() { loop { finish_cell_transitions(&mut board); print_board(&board); - update_board(&mut board); + mark_cell_transitions(&mut board); if board.population == 0 { - break; // this isn't in the original implementation but I wanted it + break; // this isn't in the original implementation but it seemed better than + // spewing blank screens } delay(); } } -fn get_pattern() -> Vec { +fn get_pattern() -> Vec> { + let max_line_len = WIDTH - 4; + let max_line_count = HEIGHT - 4; let mut lines = Vec::new(); loop { let mut line = String::new(); - // read_line reads into the buffer (appending if it's not empty). - // It returns the number of characters read, including the newline. This will be 0 on EOF. - // unwrap() will panic and terminate the program if there is an error reading from stdin. - // I think that's reasonable behavior in this case. + // read_line reads into the buffer (appending if it's not empty). It returns the + // number of characters read, including the newline. This will be 0 on EOF. + // unwrap() will panic and terminate the program if there is an error reading + // from stdin. That's reasonable behavior in this case. let nread = io::stdin().read_line(&mut line).unwrap(); let line = line.trim_end(); if nread == 0 || line.eq_ignore_ascii_case("DONE") { return lines; } - lines.push(line.to_string()); + // Handle Unicode by converting the string to a vector of characters up front. We + // do this here because we care about lengths and column alignment, so we might + // as well just do the Unicode parsing once. + let line = Vec::from_iter(line.chars()); + if line.len() > max_line_len { + println!("Line too long - the maximum is {max_line_len} characters."); + continue; + } + lines.push(line); + if lines.len() == max_line_count { + println!("Maximum line count reached. Starting simulation."); + return lines; + } } } -fn parse_pattern(rows: Vec) -> Board { - // A robust program would check the bounds of the inputs here. I'm not doing that, - // because the BASIC implementation didn't, and for me, part of the joy of these - // books back in the day was learning how my inputs could break things. +fn parse_pattern(rows: Vec>) -> Board { + // This function assumes that the input pattern in rows is in-bounds. If the pattern + // is too large, this function will panic. get_pattern checks the size of the input, + // so it is safe to call this function with its results. let mut board = Board::new(); - // Strings are UTF-8 in Rust, so characters can take multiple bytes. We will convert - // each to a Vec up front so that we don't have to do that conversion multiple - // times (to find the length of the strings in chars, then to parse each char). - // The into_iter() method consumes rows() so it can no longer be used. - let char_vecs = Vec::from_iter(rows.into_iter().map(|s| Vec::from_iter(s.chars()))); - // The BASIC implementation puts the pattern roughly in the center of the board, // assuming that there are no blank rows at the beginning or end, or blanks entered // at the beginning or end of every row. It wouldn't be hard to check for that, but // for now we'll preserve the original behavior. - let nrows = char_vecs.len(); - let ncols = char_vecs.iter() - .map(|l| l.len()) - .max() - .unwrap_or(0); // handles the case where rows is empty + let nrows = rows.len(); + let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0); // handles the case where rows is empty - // Note that there's a subtlety here. The len() method returns a usize, i.e., an - // unsigned int, so the result type is the same. If nlines >= 24 or ncols >= 68, the - // result will wrap around to a giant value. These are stricter limits than you'd - // expect from just looking at the 24x70 bounds, but again, we're preserving the - // original behavior. + // If nrows >= 24 or ncols >= 68, these assignments will wrap around to large values. + // The array accesses below will then be out of bounds. Rust will bounds-check them + // and panic rather than performing an invalid access. board.min_row = 11 - nrows / 2; board.min_col = 33 - ncols / 2; board.max_row = board.min_row + nrows - 1; board.max_col = board.min_col + ncols - 1; // Loop over the rows provided. The enumerate() method augments the iterator with an index. - for (row_index, pattern) in char_vecs.iter().enumerate() - { + for (row_index, pattern) in rows.iter().enumerate() { let row = board.min_row + row_index; // Now loop over the non-empty cells in the current row. filter_map takes a closure that // returns an Option. If the Option is None, filter_map filters out that entry from the // for loop. If it's Some(x), filter_map executes the loop body with the value x. for col in pattern.iter().enumerate().filter_map(|(col_index, chr)| { - if *chr == ' ' || (*chr == '.' && col_index == 0) { - None - } else { - Some(board.min_col + col_index) - }}) - { + if *chr == ' ' || (*chr == '.' && col_index == 0) { + None + } else { + Some(board.min_col + col_index) + } + }) { board.cells[row][col] = CellState::Alive; board.population += 1; } } - board } @@ -214,15 +216,16 @@ fn print_board(board: &Board) { } fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { + // Simply loop over all the immediate neighbors of a cell. We assume that the row and + // column indices are not on (or outside) the boundary of the arrays; if they are, + // the function will panic instead of going out of bounds. let mut count = 0; - assert!((1..=HEIGHT-2).contains(&row_index)); - assert!((1..=WIDTH-2).contains(&col_index)); for i in row_index-1..=row_index+1 { for j in col_index-1..=col_index+1 { if i == row_index && j == col_index { continue; } - if board.cells[i][j] == CellState::Alive || board.cells [i][j] == CellState::AboutToDie { + if board.cells[i][j] == CellState::Alive || board.cells[i][j] == CellState::AboutToDie { count += 1; } } @@ -230,7 +233,7 @@ fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 { count } -fn update_board(board: &mut Board) { +fn mark_cell_transitions(board: &mut Board) { for row_index in board.min_row-1..=board.max_row+1 { for col_index in board.min_col-1..=board.max_col+1 { let neighbors = count_neighbors(board, row_index, col_index); @@ -238,7 +241,7 @@ fn update_board(board: &mut Board) { *this_cell_state = match *this_cell_state { CellState::Empty if neighbors == 3 => CellState::AboutToBeBorn, CellState::Alive if !(2..=3).contains(&neighbors) => CellState::AboutToDie, - _ => *this_cell_state + _ => *this_cell_state, } } } From 6e46aba249410a1b0e88ae09406b60e5c0433dae Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:34:40 -0400 Subject: [PATCH 17/21] README file for the Rust port --- 55_Life/rust/README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 55_Life/rust/README.md diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md new file mode 100644 index 00000000..7bd5dd22 --- /dev/null +++ b/55_Life/rust/README.md @@ -0,0 +1,20 @@ +# Conway's Life + +Original from David Ahl's _Basic Computer Games_, downloaded from http://www.vintage-basic.net/games.html. + +Ported to Rust by Jon Fetter-Degges + +Developed and tested on Rust 1.64.0 + +## How to Run + +Install Rust using the instructions at [rust-lang.org](https://www.rust-lang.org/tools/install). + +At a command or shell prompt in the `rust` subdirectory, enter `cargo run`. + +## Differences from Original Behavior + +* The simulation stops if all cells die. +* Input of more than 66 columns is rejected. Input will automatically terminate after 20 rows. Beyond these bounds, the original +implementation would have marked the board as invalid, and beyond 68 cols/24 rows it would have had an out of bounds array access. +* The check for the string "DONE" at the end of input is case-independent. From 14e59ac5fe3a86a2fde1466859865a255cd999ab Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:37:05 -0400 Subject: [PATCH 18/21] Improve printing, make output closer to original Implemented Display for CellState, and tweaked outputs to match the BASIC implementation. Also fixed some more comments. --- 55_Life/rust/src/main.rs | 59 ++++++++++++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index 37e0f6c0..0cfe707f 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,12 +1,24 @@ -use std::{io, thread, time}; +// Rust implementation of David Ahl's implementation of Conway's Life +// +// Jon Fetter-Degges +// October 2022 + +// I am a Rust newbie. Corrections and suggestions are welcome. + +use std::{fmt, io, thread, time}; const HEIGHT: usize = 24; const WIDTH: usize = 70; // The BASIC implementation uses a 24x70 array of integers to represent the board state. -// 1 is alive, 2 is about to die, 3 is about to be born, all other values are dead. -// (I'm not actually sure whether there are other values besides zero.) -// Here, we'll use an enum instead. +// 1 is alive, 2 is about to die, 3 is about to be born, 0 is dead. Here, we'll use an +// enum instead. +// Deriving Copy (which requires Clone) allows us to use this enum value in assignments. +// Without that we would only be able to borrow it. That seems silly for a simple enum +// like this one - it is required because enums can have large amounts of associated +// data, so the programmer needs to decide whether to allow copying. Similarly, PartialEq +// allows use of the == comparison. Again, this seems silly for a simple enum, but if +// some enum cases have associated data, it may require some thought. #[derive(Clone, Copy, PartialEq)] enum CellState { Empty, @@ -15,6 +27,20 @@ enum CellState { AboutToBeBorn, } +// Support direct printing of the cell. In this program cells will only be Alive or Empty +// when they are printed. +impl fmt::Display for CellState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let rep = match *self { + CellState::Empty => ' ', + CellState::Alive => '*', + CellState::AboutToDie => 'o', + CellState::AboutToBeBorn => '.', + }; + write!(f, "{}", rep) + } +} + // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. // Since that isn't too big (even in the 70's), we just store the whole board as an // array of CellState. I'm experimenting with using an array-of-arrays to make references @@ -39,7 +65,7 @@ impl Board { min_col: 0, max_col: 0, population: 0, - generation: 1, + generation: 0, invalid: false, } } @@ -47,6 +73,8 @@ impl Board { fn main() { println!(); println!(); println!(); + println!("{:33}{}", " ", "Life"); + println!("{:14}{}", " ", "Creative Computing Morristown, New Jersey"); println!("Enter your pattern: "); let mut board = parse_pattern(get_pattern()); loop { @@ -104,11 +132,14 @@ fn parse_pattern(rows: Vec>) -> Board { // at the beginning or end of every row. It wouldn't be hard to check for that, but // for now we'll preserve the original behavior. let nrows = rows.len(); - let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0); // handles the case where rows is empty + // If rows is empty, the call to max will return None. The unwrap_or then provides a + // default value + let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0); - // If nrows >= 24 or ncols >= 68, these assignments will wrap around to large values. - // The array accesses below will then be out of bounds. Rust will bounds-check them - // and panic rather than performing an invalid access. + // The min and max values here are unsigned. If nrows >= 24 or ncols >= 68, these + // assignments will panic - they do not wrap around unless we use a function with + // that specific behavior. Again, we expect bounds checking on the input before this + // function is called. board.min_row = 11 - nrows / 2; board.min_col = 33 - ncols / 2; board.max_row = board.min_row + nrows - 1; @@ -201,15 +232,15 @@ fn finish_cell_transitions(board: &mut Board) { fn print_board(board: &Board) { println!(); println!(); println!(); - println!("Generation: {}", board.generation); - println!("Population: {}", board.population); + print!("Generation: {} Population: {}", board.generation, board.population); if board.invalid { - println!("Invalid!"); + print!("Invalid!"); } + println!(); for row_index in 0..HEIGHT { for col_index in 0..WIDTH { - let rep = if board.cells[row_index][col_index] == CellState::Alive { "*" } else { " " }; - print!("{rep}"); + // This print will use the Display implementation for cell_state, above. + print!("{}", board.cells[row_index][col_index]); } println!(); } From 5214f2a68117d3964ab79d9bdfe0382546aaa8ed Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 21:49:40 -0400 Subject: [PATCH 19/21] One more implementation note --- 55_Life/rust/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md index 7bd5dd22..3cce8366 100644 --- a/55_Life/rust/README.md +++ b/55_Life/rust/README.md @@ -15,6 +15,7 @@ At a command or shell prompt in the `rust` subdirectory, enter `cargo run`. ## Differences from Original Behavior * The simulation stops if all cells die. +* `.` at the beginning of an input line is supported but optional. * Input of more than 66 columns is rejected. Input will automatically terminate after 20 rows. Beyond these bounds, the original implementation would have marked the board as invalid, and beyond 68 cols/24 rows it would have had an out of bounds array access. * The check for the string "DONE" at the end of input is case-independent. From 7b929ecbb1cd7ee43acc1e01423f5f37737e126e Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 22:03:57 -0400 Subject: [PATCH 20/21] Small fixes and use min/max --- 55_Life/rust/README.md | 1 + 55_Life/rust/src/main.rs | 43 +++++++++++++++++----------------------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md index 3cce8366..19f2d09d 100644 --- a/55_Life/rust/README.md +++ b/55_Life/rust/README.md @@ -19,3 +19,4 @@ At a command or shell prompt in the `rust` subdirectory, enter `cargo run`. * Input of more than 66 columns is rejected. Input will automatically terminate after 20 rows. Beyond these bounds, the original implementation would have marked the board as invalid, and beyond 68 cols/24 rows it would have had an out of bounds array access. * The check for the string "DONE" at the end of input is case-independent. +* The program pauses for half a second between each generation. diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index 0cfe707f..0c1e83cc 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -5,7 +5,7 @@ // I am a Rust newbie. Corrections and suggestions are welcome. -use std::{fmt, io, thread, time}; +use std::{cmp, fmt, io, thread, time}; const HEIGHT: usize = 24; const WIDTH: usize = 70; @@ -42,9 +42,8 @@ impl fmt::Display for CellState { } // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. -// Since that isn't too big (even in the 70's), we just store the whole board as an -// array of CellState. I'm experimenting with using an array-of-arrays to make references -// more convenient. +// The board is an array of CellState. Using an array of arrays gives us bounds checking +// in both dimensions. struct Board { cells: [[CellState; WIDTH]; HEIGHT], min_row: usize, @@ -145,12 +144,13 @@ fn parse_pattern(rows: Vec>) -> Board { board.max_row = board.min_row + nrows - 1; board.max_col = board.min_col + ncols - 1; - // Loop over the rows provided. The enumerate() method augments the iterator with an index. + // Loop over the rows provided. enumerate() augments the iterator with an index. for (row_index, pattern) in rows.iter().enumerate() { let row = board.min_row + row_index; - // Now loop over the non-empty cells in the current row. filter_map takes a closure that - // returns an Option. If the Option is None, filter_map filters out that entry from the - // for loop. If it's Some(x), filter_map executes the loop body with the value x. + // Now loop over the non-empty cells in the current row. filter_map takes a + // closure that returns an Option. If the Option is None, filter_map filters out + // that entry from the for loop. If it's Some(x), filter_map executes the loop + // body with the value x. for col in pattern.iter().enumerate().filter_map(|(col_index, chr)| { if *chr == ' ' || (*chr == '.' && col_index == 0) { None @@ -186,22 +186,14 @@ fn finish_cell_transitions(board: &mut Board) { } if *cell == CellState::Alive { any_alive_this_row = true; - if min_col > col_index { - min_col = col_index; - } - if max_col < col_index { - max_col = col_index; - } + min_col = cmp::min(min_col, col_index); + max_col = cmp::max(max_col, col_index); } } if any_alive_this_row { - if min_row > row_index { - min_row = row_index; - } - if max_row < row_index { - max_row = row_index; - } - } + min_row = cmp::min(min_row, row_index); + max_row = cmp::max(max_row, row_index); + } } // If anything is alive within two cells of the boundary, mark the board invalid and // clamp the bounds. We need a two-cell margin because we'll count neighbors on cells @@ -232,14 +224,14 @@ fn finish_cell_transitions(board: &mut Board) { fn print_board(board: &Board) { println!(); println!(); println!(); - print!("Generation: {} Population: {}", board.generation, board.population); + print!("Generation: {} Population: {}", board.generation, board.population); if board.invalid { - print!("Invalid!"); + print!(" Invalid!"); } println!(); for row_index in 0..HEIGHT { for col_index in 0..WIDTH { - // This print will use the Display implementation for cell_state, above. + // This print uses the Display implementation for cell_state, above. print!("{}", board.cells[row_index][col_index]); } println!(); @@ -268,7 +260,8 @@ fn mark_cell_transitions(board: &mut Board) { for row_index in board.min_row-1..=board.max_row+1 { for col_index in board.min_col-1..=board.max_col+1 { let neighbors = count_neighbors(board, row_index, col_index); - let this_cell_state = &mut board.cells[row_index][col_index]; // borrow a mutable reference to the array cell + // Borrow a mutable reference to the array cell + let this_cell_state = &mut board.cells[row_index][col_index]; *this_cell_state = match *this_cell_state { CellState::Empty if neighbors == 3 => CellState::AboutToBeBorn, CellState::Alive if !(2..=3).contains(&neighbors) => CellState::AboutToDie, From 5e3e7d60aee112dcccd4822baa4ff8345be4f6fb Mon Sep 17 00:00:00 2001 From: Jon Fetter-Degges Date: Tue, 11 Oct 2022 22:18:32 -0400 Subject: [PATCH 21/21] couple more comment changes --- 55_Life/rust/src/main.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs index 0c1e83cc..50aa4927 100644 --- a/55_Life/rust/src/main.rs +++ b/55_Life/rust/src/main.rs @@ -1,4 +1,4 @@ -// Rust implementation of David Ahl's implementation of Conway's Life +// Rust implementation of the "Basic Computer Games" version of Conway's Life // // Jon Fetter-Degges // October 2022 @@ -7,19 +7,14 @@ use std::{cmp, fmt, io, thread, time}; -const HEIGHT: usize = 24; -const WIDTH: usize = 70; - -// The BASIC implementation uses a 24x70 array of integers to represent the board state. -// 1 is alive, 2 is about to die, 3 is about to be born, 0 is dead. Here, we'll use an -// enum instead. -// Deriving Copy (which requires Clone) allows us to use this enum value in assignments. -// Without that we would only be able to borrow it. That seems silly for a simple enum -// like this one - it is required because enums can have large amounts of associated -// data, so the programmer needs to decide whether to allow copying. Similarly, PartialEq -// allows use of the == comparison. Again, this seems silly for a simple enum, but if -// some enum cases have associated data, it may require some thought. -#[derive(Clone, Copy, PartialEq)] +// The BASIC implementation uses integers to represent the state of each cell: 1 is +// alive, 2 is about to die, 3 is about to be born, 0 is dead. Here, we'll use an enum +// instead. +// Deriving Copy (which requires Clone) allows us to use this enum value in assignments, +// and deriving Eq (or PartialEq) allows us to use the == operator. These need to be +// explicitly specified because some enums may have associated data that makes copies and +// comparisons more complicated or expensive. +#[derive(Clone, Copy, PartialEq, Eq)] enum CellState { Empty, Alive, @@ -44,6 +39,9 @@ impl fmt::Display for CellState { // Following the BASIC implementation, we will bound the board at 24 rows x 70 columns. // The board is an array of CellState. Using an array of arrays gives us bounds checking // in both dimensions. +const HEIGHT: usize = 24; +const WIDTH: usize = 70; + struct Board { cells: [[CellState; WIDTH]; HEIGHT], min_row: usize, @@ -104,8 +102,8 @@ fn get_pattern() -> Vec> { return lines; } // Handle Unicode by converting the string to a vector of characters up front. We - // do this here because we care about lengths and column alignment, so we might - // as well just do the Unicode parsing once. + // do this here because we check the number of characters several times, so we + // might as well just do the Unicode parsing once. let line = Vec::from_iter(line.chars()); if line.len() > max_line_len { println!("Line too long - the maximum is {max_line_len} characters.");