diff --git a/00_Utilities/find-unimplemented.js b/00_Utilities/find-unimplemented.js new file mode 100644 index 00000000..41641f52 --- /dev/null +++ b/00_Utilities/find-unimplemented.js @@ -0,0 +1,73 @@ +/** + * Program to show unimplemented games by language, optionally filtered by + * language + * + * Usage: node find-unimplemented.js [[[lang1] lang2] ...] + * + * Adapted from find-missing-implementtion.js + */ + +const fs = require("fs"); +const glob = require("glob"); + +// relative path to the repository root +const ROOT_PATH = "../."; + +let languages = [ + { name: "csharp", extension: "cs" }, + { name: "java", extension: "java" }, + { name: "javascript", extension: "html" }, + { name: "pascal", extension: "pas" }, + { name: "perl", extension: "pl" }, + { name: "python", extension: "py" }, + { name: "ruby", extension: "rb" }, + { name: "vbnet", extension: "vb" }, +]; + +const getFilesRecursive = async (path, extension) => { + return new Promise((resolve, reject) => { + glob(`${path}/**/*.${extension}`, (err, matches) => { + if (err) { + reject(err); + } + resolve(matches); + }); + }); +}; + +const getPuzzleFolders = () => { + return fs + .readdirSync(ROOT_PATH, { withFileTypes: true }) + .filter((dirEntry) => dirEntry.isDirectory()) + .filter( + (dirEntry) => + ![".git", "node_modules", "00_Utilities", "buildJvm"].includes(dirEntry.name) + ) + .map((dirEntry) => dirEntry.name); +}; + +(async () => { + const result = {}; + if (process.argv.length > 2) { + languages = languages.filter((language) => process.argv.slice(2).includes(language.name)); + } + for (const { name: language } of languages) { + result[language] = []; + } + + const puzzleFolders = getPuzzleFolders(); + for (const puzzleFolder of puzzleFolders) { + for (const { name: language, extension } of languages) { + const files = await getFilesRecursive( + `${ROOT_PATH}/${puzzleFolder}/${language}`, extension + ); + if (files.length === 0) { + result[language].push(puzzleFolder); + } + } + } + console.log('Unimplementation by language:') + console.dir(result); +})(); + +return; diff --git a/03_Animal/java/src/Animal.java b/03_Animal/java/src/Animal.java index c4222c5f..1e489559 100644 --- a/03_Animal/java/src/Animal.java +++ b/03_Animal/java/src/Animal.java @@ -10,6 +10,8 @@ import java.util.stream.Collectors; * Converted from BASIC to Java by Aldrin Misquitta (@aldrinm) * The original BASIC program uses an array to maintain the questions and answers and to decide which question to * ask next. Updated this Java implementation to use a tree instead of the earlier faulty one based on a list (thanks @patimen). + * + * Bonus option: TREE --> prints the game decision data as a tree to visualize/debug the state of the game */ public class Animal { diff --git a/06_Banner/ruby/banner.rb b/06_Banner/ruby/banner.rb new file mode 100755 index 00000000..53136dbd --- /dev/null +++ b/06_Banner/ruby/banner.rb @@ -0,0 +1,92 @@ +#!/usr/bin/env ruby + +# Banner +# reinterpreted from BASIC by stephan.com + +# this implementation diverges from the original in some notable +# ways, but maintains the same font definition as before as well +# as the same somewhat bizarre way of interpreting it. It would +# be more efficient to redesign the font to allow `"%09b" % row` +# and then some substitutions. + +FONT = { + ' ' => [0, 0, 0, 0, 0, 0, 0].freeze, + '!' => [1, 1, 1, 384, 1, 1, 1].freeze, + '*' => [69, 41, 17, 512, 17, 41, 69].freeze, + '.' => [1, 1, 129, 449, 129, 1, 1].freeze, + '0' => [57, 69, 131, 258, 131, 69, 57].freeze, + '1' => [0, 0, 261, 259, 512, 257, 257].freeze, + '2' => [261, 387, 322, 290, 274, 267, 261].freeze, + '3' => [66, 130, 258, 274, 266, 150, 100].freeze, + '4' => [33, 49, 41, 37, 35, 512, 33].freeze, + '5' => [160, 274, 274, 274, 274, 274, 226].freeze, + '6' => [194, 291, 293, 297, 305, 289, 193].freeze, + '7' => [258, 130, 66, 34, 18, 10, 8].freeze, + '8' => [69, 171, 274, 274, 274, 171, 69].freeze, + '9' => [263, 138, 74, 42, 26, 10, 7].freeze, + '=' => [41, 41, 41, 41, 41, 41, 41].freeze, + '?' => [5, 3, 2, 354, 18, 11, 5].freeze, + 'a' => [505, 37, 35, 34, 35, 37, 505].freeze, + 'b' => [512, 274, 274, 274, 274, 274, 239].freeze, + 'c' => [125, 131, 258, 258, 258, 131, 69].freeze, + 'd' => [512, 258, 258, 258, 258, 131, 125].freeze, + 'e' => [512, 274, 274, 274, 274, 258, 258].freeze, + 'f' => [512, 18, 18, 18, 18, 2, 2].freeze, + 'g' => [125, 131, 258, 258, 290, 163, 101].freeze, + 'h' => [512, 17, 17, 17, 17, 17, 512].freeze, + 'i' => [258, 258, 258, 512, 258, 258, 258].freeze, + 'j' => [65, 129, 257, 257, 257, 129, 128].freeze, + 'k' => [512, 17, 17, 41, 69, 131, 258].freeze, + 'l' => [512, 257, 257, 257, 257, 257, 257].freeze, + 'm' => [512, 7, 13, 25, 13, 7, 512].freeze, + 'n' => [512, 7, 9, 17, 33, 193, 512].freeze, + 'o' => [125, 131, 258, 258, 258, 131, 125].freeze, + 'p' => [512, 18, 18, 18, 18, 18, 15].freeze, + 'q' => [125, 131, 258, 258, 322, 131, 381].freeze, + 'r' => [512, 18, 18, 50, 82, 146, 271].freeze, + 's' => [69, 139, 274, 274, 274, 163, 69].freeze, + 't' => [2, 2, 2, 512, 2, 2, 2].freeze, + 'u' => [128, 129, 257, 257, 257, 129, 128].freeze, + 'v' => [64, 65, 129, 257, 129, 65, 64].freeze, + 'w' => [256, 257, 129, 65, 129, 257, 256].freeze, + 'x' => [388, 69, 41, 17, 41, 69, 388].freeze, + 'y' => [8, 9, 17, 481, 17, 9, 8].freeze, + 'z' => [386, 322, 290, 274, 266, 262, 260].freeze +}.freeze + +puts 'horizontal' +x = gets.strip.to_i +puts 'vertical' +y = gets.strip.to_i +puts 'centered' +centered = gets.strip.downcase.chars.first == 'y' +puts 'character ("all" for character being printed)' +fill = gets.strip.downcase +puts 'statement' +statement = gets.strip.downcase + +all = (fill.downcase == 'all') +lenxs = all ? 1 : fill.length +start = 1 +start += (63 - 4.5 * y) / lenxs if centered + +statement.each_char do |char| + next puts "\n" * 7 * x if char == ' ' + + xs = all ? char : fill + FONT[char].each do |su| + print ' ' * start + 8.downto(0) do |k| + if (1 << k) < su + print xs * y + su -= (1 << k) + else + print ' ' * (y * lenxs) + end + end + puts + end + + (2 * x).times { puts } +end +75.times { puts } diff --git a/14_Bowling/csharp/Bowling.cs b/14_Bowling/csharp/Bowling.cs new file mode 100644 index 00000000..16ab067e --- /dev/null +++ b/14_Bowling/csharp/Bowling.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + public class Bowling + { + private readonly Pins pins = new(); + + private int players; + + public void Play() + { + ShowBanner(); + MaybeShowInstructions(); + Setup(); + GameLoop(); + } + + private static void ShowBanner() + { + Utility.PrintString(34, "BOWL"); + Utility.PrintString(15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + Utility.PrintString(); + Utility.PrintString(); + Utility.PrintString(); + Utility.PrintString("WELCOME TO THE ALLEY"); + Utility.PrintString("BRING YOUR FRIENDS"); + Utility.PrintString("OKAY LET'S FIRST GET ACQUAINTED"); + Utility.PrintString(); + } + private static void MaybeShowInstructions() + { + Utility.PrintString("THE INSTRUCTIONS (Y/N)"); + if (Utility.InputString() == "N") return; + Utility.PrintString("THE GAME OF BOWLING TAKES MIND AND SKILL.DURING THE GAME"); + Utility.PrintString("THE COMPUTER WILL KEEP SCORE.YOU MAY COMPETE WITH"); + Utility.PrintString("OTHER PLAYERS[UP TO FOUR].YOU WILL BE PLAYING TEN FRAMES"); + Utility.PrintString("ON THE PIN DIAGRAM 'O' MEANS THE PIN IS DOWN...'+' MEANS THE"); + Utility.PrintString("PIN IS STANDING.AFTER THE GAME THE COMPUTER WILL SHOW YOUR"); + Utility.PrintString("SCORES ."); + } + private void Setup() + { + Utility.PrintString("FIRST OF ALL...HOW MANY ARE PLAYING", false); + var input = Utility.InputInt(); + players = input < 1 ? 1 : input; + Utility.PrintString(); + Utility.PrintString("VERY GOOD..."); + } + private void GameLoop() + { + GameResults[] gameResults = InitGameResults(); + var done = false; + while (!done) + { + ResetGameResults(gameResults); + for (int frame = 0; frame < GameResults.FramesPerGame; ++frame) + { + for (int player = 0; player < players; ++player) + { + pins.Reset(); + int pinsDownThisFrame = pins.GetPinsDown(); + + int ball = 1; + while (ball == 1 || ball == 2) // One or two rolls + { + Utility.PrintString("TYPE ROLL TO GET THE BALL GOING."); + _ = Utility.InputString(); + + int pinsDownAfterRoll = pins.Roll(); + ShowPins(player, frame, ball); + + if (pinsDownAfterRoll == pinsDownThisFrame) + { + Utility.PrintString("GUTTER!!"); + } + + if (ball == 1) + { + // Store current pin count + gameResults[player].Results[frame].PinsBall1 = pinsDownAfterRoll; + + // Special handling for strike + if (pinsDownAfterRoll == Pins.TotalPinCount) + { + Utility.PrintString("STRIKE!!!!!\a\a\a\a"); + // No second roll + ball = 0; + gameResults[player].Results[frame].PinsBall2 = pinsDownAfterRoll; + gameResults[player].Results[frame].Score = FrameResult.Points.Strike; + } + else + { + ball = 2; // Roll again + Utility.PrintString("ROLL YOUR SECOND BALL"); + } + } + else if (ball == 2) + { + // Store current pin count + gameResults[player].Results[frame].PinsBall2 = pinsDownAfterRoll; + ball = 0; + + // Determine the score for the frame + if (pinsDownAfterRoll == Pins.TotalPinCount) + { + Utility.PrintString("SPARE!!!!"); + gameResults[player].Results[frame].Score = FrameResult.Points.Spare; + } + else + { + Utility.PrintString("ERROR!!!"); + gameResults[player].Results[frame].Score = FrameResult.Points.Error; + } + } + Utility.PrintString(); + } + } + } + ShowGameResults(gameResults); + Utility.PrintString("DO YOU WANT ANOTHER GAME"); + var a = Utility.InputString(); + done = a.Length == 0 || a[0] != 'Y'; + } + } + + private GameResults[] InitGameResults() + { + var gameResults = new GameResults[players]; + for (int i = 0; i < gameResults.Length; i++) + { + gameResults[i] = new GameResults(); + } + return gameResults; + } + + private void ShowPins(int player, int frame, int ball) + { + Utility.PrintString($"FRAME: {frame + 1} PLAYER: {player + 1} BALL: {ball}"); + var breakPins = new bool[] { true, false, false, false, true, false, false, true, false, true }; + var indent = 0; + for (int pin = 0; pin < Pins.TotalPinCount; ++pin) + { + if (breakPins[pin]) + { + Utility.PrintString(); // End row + Utility.PrintString(indent++, false); // Indent next row + } + var s = pins[pin] == Pins.State.Down ? "+ " : "o "; + Utility.PrintString(s, false); + } + Utility.PrintString(); + Utility.PrintString(); + } + private void ResetGameResults(GameResults[] gameResults) + { + foreach (var gameResult in gameResults) + { + foreach (var frameResult in gameResult.Results) + { + frameResult.Reset(); + } + } + } + private void ShowGameResults(GameResults[] gameResults) + { + Utility.PrintString("FRAMES"); + for (int i = 0; i < GameResults.FramesPerGame; ++i) + { + Utility.PrintString(Utility.PadInt(i, 3), false); + } + Utility.PrintString(); + foreach (var gameResult in gameResults) + { + foreach (var frameResult in gameResult.Results) + { + Utility.PrintString(Utility.PadInt(frameResult.PinsBall1, 3), false); + } + Utility.PrintString(); + foreach (var frameResult in gameResult.Results) + { + Utility.PrintString(Utility.PadInt(frameResult.PinsBall2, 3), false); + } + Utility.PrintString(); + foreach (var frameResult in gameResult.Results) + { + Utility.PrintString(Utility.PadInt((int)frameResult.Score, 3), false); + } + Utility.PrintString(); + Utility.PrintString(); + } + } + } +} diff --git a/14_Bowling/csharp/FrameResult.cs b/14_Bowling/csharp/FrameResult.cs new file mode 100644 index 00000000..c28b5c0f --- /dev/null +++ b/14_Bowling/csharp/FrameResult.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + public class FrameResult + { + public enum Points { None, Error, Spare, Strike }; + + public int PinsBall1 { get; set; } + public int PinsBall2 { get; set; } + public Points Score { get; set; } + + public void Reset() + { + PinsBall1 = PinsBall2 = 0; + Score = Points.None; + } + } +} diff --git a/14_Bowling/csharp/GameResults.cs b/14_Bowling/csharp/GameResults.cs new file mode 100644 index 00000000..10f1e735 --- /dev/null +++ b/14_Bowling/csharp/GameResults.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + public class GameResults + { + public static readonly int FramesPerGame = 10; + public FrameResult[] Results { get; set; } + + public GameResults() + { + Results = new FrameResult[FramesPerGame]; + for (int i = 0; i < FramesPerGame; ++i) + { + Results[i] = new FrameResult(); + } + } + } +} diff --git a/14_Bowling/csharp/Pins.cs b/14_Bowling/csharp/Pins.cs new file mode 100644 index 00000000..5321b530 --- /dev/null +++ b/14_Bowling/csharp/Pins.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + public class Pins + { + public enum State { Up, Down }; + public static readonly int TotalPinCount = 10; + private readonly Random random = new(); + + private State[] PinSet { get; set; } + + public Pins() + { + PinSet = new State[TotalPinCount]; + } + public State this[int i] + { + get { return PinSet[i]; } + set { PinSet[i] = value; } + } + public int Roll() + { + // REM ARK BALL GENERATOR USING MOD '15' SYSTEM + for (int i = 0; i < 20; ++i) + { + var x = random.Next(100) + 1; + int j; + for (j = 1; j <= 10; ++j) + { + if (x < 15 * j) + break; + } + var pindex = 15 * j - x; + if (pindex > 0 && pindex <= TotalPinCount) + PinSet[--pindex] = State.Down; + } + return GetPinsDown(); + } + public void Reset() + { + for (int i = 0; i < PinSet.Length; ++i) + { + PinSet[i] = State.Up; + } + } + public int GetPinsDown() + { + return PinSet.Count(p => p == State.Down); + } + } +} diff --git a/14_Bowling/csharp/Program.cs b/14_Bowling/csharp/Program.cs new file mode 100644 index 00000000..85e058b7 --- /dev/null +++ b/14_Bowling/csharp/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + public static class Program + { + public static void Main() + { + new Bowling().Play(); + } + } +} diff --git a/14_Bowling/csharp/Utility.cs b/14_Bowling/csharp/Utility.cs new file mode 100644 index 00000000..09687232 --- /dev/null +++ b/14_Bowling/csharp/Utility.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bowling +{ + internal static class Utility + { + public static string PadInt(int value, int width) + { + return value.ToString().PadLeft(width); + } + public static int InputInt() + { + while (true) + { + if (int.TryParse(InputString(), out int i)) + return i; + else + PrintString("!NUMBER EXPECTED - RETRY INPUT LINE"); + } + } + public static string InputString() + { + PrintString("? ", false); + var input = Console.ReadLine(); + return input == null ? string.Empty : input.ToUpper(); + } + public static void PrintInt(int value, bool newLine = false) + { + PrintString($"{value} ", newLine); + } + public static void PrintString(bool newLine = true) + { + PrintString(0, string.Empty); + } + public static void PrintString(int tab, bool newLine = true) + { + PrintString(tab, string.Empty, newLine); + } + public static void PrintString(string value, bool newLine = true) + { + PrintString(0, value, newLine); + } + public static void PrintString(int tab, string value, bool newLine = true) + { + Console.Write(new String(' ', tab)); + Console.Write(value); + if (newLine) Console.WriteLine(); + } + } +} diff --git a/19_Bunny/csharp/BasicData.cs b/19_Bunny/csharp/BasicData.cs new file mode 100644 index 00000000..76647a56 --- /dev/null +++ b/19_Bunny/csharp/BasicData.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bunny +{ + internal class BasicData + { + private readonly int[] data; + + private int index; + + public BasicData(int[] data) + { + this.data = data; + index = 0; + } + public int Read() + { + return data[index++]; + } + } +} diff --git a/19_Bunny/csharp/Bunny.cs b/19_Bunny/csharp/Bunny.cs new file mode 100644 index 00000000..76e5e7e4 --- /dev/null +++ b/19_Bunny/csharp/Bunny.cs @@ -0,0 +1,87 @@ +namespace Bunny +{ + internal class Bunny + { + private const int asciiBase = 64; + private readonly int[] bunnyData = { + 2,21,14,14,25, + 1,2,-1,0,2,45,50,-1,0,5,43,52,-1,0,7,41,52,-1, + 1,9,37,50,-1,2,11,36,50,-1,3,13,34,49,-1,4,14,32,48,-1, + 5,15,31,47,-1,6,16,30,45,-1,7,17,29,44,-1,8,19,28,43,-1, + 9,20,27,41,-1,10,21,26,40,-1,11,22,25,38,-1,12,22,24,36,-1, + 13,34,-1,14,33,-1,15,31,-1,17,29,-1,18,27,-1, + 19,26,-1,16,28,-1,13,30,-1,11,31,-1,10,32,-1, + 8,33,-1,7,34,-1,6,13,16,34,-1,5,12,16,35,-1, + 4,12,16,35,-1,3,12,15,35,-1,2,35,-1,1,35,-1, + 2,34,-1,3,34,-1,4,33,-1,6,33,-1,10,32,34,34,-1, + 14,17,19,25,28,31,35,35,-1,15,19,23,30,36,36,-1, + 14,18,21,21,24,30,37,37,-1,13,18,23,29,33,38,-1, + 12,29,31,33,-1,11,13,17,17,19,19,22,22,24,31,-1, + 10,11,17,18,22,22,24,24,29,29,-1, + 22,23,26,29,-1,27,29,-1,28,29,-1,4096 + }; + + public void Run() + { + PrintString(33, "BUNNY"); + PrintString(15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + PrintLines(3); + + // Set up a BASIC-ish data object + BasicData data = new (bunnyData); + + // Get the first five data values into an array. + // These are the characters we are going to print. + // Unlike the original program, we are only converting + // them to ASCII once. + var a = new char[5]; + for (var i = 0; i < 5; ++i) + { + a[i] = (char)(asciiBase + data.Read()); + } + PrintLines(6); + + PrintLines(1); + var col = 0; + while (true) + { + var x = data.Read(); + if (x < 0) // Start a new line + { + PrintLines(1); + col = 0; + continue; + } + if (x > 128) break; // End processing + col += PrintSpaces(x - col); // Move to TAB position x (sort of) + var y = data.Read(); // Read the next value + for (var i = x; i <= y; ++i) + { + // var j = i - 5 * (i / 5); // BASIC didn't have a modulus operator + Console.Write(a[i % 5]); + // Console.Write(a[col % 5]); // This works, too + ++col; + } + } + PrintLines(6); + } + private static void PrintLines(int count) + { + for (var i = 0; i < count; ++i) + Console.WriteLine(); + } + private static int PrintSpaces(int count) + { + for (var i = 0; i < count; ++i) + Console.Write(' '); + return count; + } + public static void PrintString(int tab, string value, bool newLine = true) + { + PrintSpaces(tab); + Console.Write(value); + if (newLine) Console.WriteLine(); + } + + } +} diff --git a/19_Bunny/csharp/Program.cs b/19_Bunny/csharp/Program.cs new file mode 100644 index 00000000..25604a51 --- /dev/null +++ b/19_Bunny/csharp/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Bunny +{ + public static class Program + { + public static void Main() + { + new Bunny().Run(); + } + } +} diff --git a/52_Kinema/ruby/kinema.rb b/52_Kinema/ruby/kinema.rb new file mode 100644 index 00000000..27ed2aa2 --- /dev/null +++ b/52_Kinema/ruby/kinema.rb @@ -0,0 +1,45 @@ +#!/usr/bin/env ruby + +# Kinema +# reinterpreted from BASIC by stephan.com + +EPSILON = 0.15 + +def close?(guess, answer) + (guess-answer).abs < answer * EPSILON +end + +def ask(text, answer) + puts text + guess = gets.strip.to_f + if close?(guess, answer) + puts 'Close enough' + @score += 1 + else + puts 'Not even close....' + end + + puts "Correct answer is #{answer}" +end + +puts 'Kinema'.center(80) +puts 'Adapted by stephan.com'.center(80) +puts; puts; puts; + +loop do + puts; puts + @score = 0 + v = 5 + rand(35) + + puts "A ball is thrown upwards at #{v} meters per second" + + ask 'How high will it go? (in meters)', 0.05 * v * v + ask 'How long until it returns? (in seconds)', v/5.0 + + t = 1 + rand(2*v)/10.0 + ask "What will its velocity be after #{t} seconds?", v - 10 * t + puts + print "#{@score} right out of 3." + print " not bad" if @score > 1 + puts +end diff --git a/54_Letter/ruby/letter.rb b/54_Letter/ruby/letter.rb new file mode 100644 index 00000000..c4c7a00c --- /dev/null +++ b/54_Letter/ruby/letter.rb @@ -0,0 +1,45 @@ +#!/usr/bin/env ruby + +# Kinema +# reinterpreted from BASIC by stephan.com + +puts 'Letter'.center(80) +puts 'Adapted by stephan.com'.center(80) +puts "\n\n\n" + +puts "Letter guessing game\n\n" + +puts "I'll think of a letter of the alphabet, A to Z." +puts "Try to guess my letter and I'll give you clues" +puts "as to how close you're getting to my letter." + +def win(turns) + puts "\nyou got it in #{turns} guesses!!" + return puts "but it shouldn't take more than 5 guesses!" if turns > 5 + + puts "good job !!!!!\a\a\a" +end + +def play + letter = ('A'..'Z').to_a.sample + guess = nil + turn = 0 + + puts "\nO.K., I have a letter. Start guessing." + + until guess == letter + puts "\nWhat is your guess?" + + guess = gets.strip.chars.first.upcase + turn += 1 + + puts 'Too low. Try a higher letter.' if guess < letter + puts 'Too high. Try a lower letter.' if guess > letter + end + win(turn) +end + +loop do + play + puts "\nlet's play again....." +end diff --git a/58_Love/ruby/love.rb b/58_Love/ruby/love.rb new file mode 100644 index 00000000..3717a14b --- /dev/null +++ b/58_Love/ruby/love.rb @@ -0,0 +1,43 @@ +data = [60, 1, 12, 26, 9, 12, 3, 8, 24, 17, 8, 4, 6, 23, 21, 6, 4, 6, 22, 12, 5, 6, 5, + 4, 6, 21, 11, 8, 6, 4, 4, 6, 21, 10, 10, 5, 4, 4, 6, 21, 9, 11, 5, 4, 4, 6, 21, + 8, 11, 6, 4, 4, 6, 21, 7, 11, 7, 4, 4, 6, 21, 6, 11, 8, 4, 4, 6, 19, 1, 1, 5, + 11, 9, 4, 4, 6, 19, 1, 1, 5, 10, 10, 4, 4, 6, 18, 2, 1, 6, 8, 11, 4, 4, 6, 17, + 3, 1, 7, 5, 13, 4, 4, 6, 15, 5, 2, 23, 5, 1, 29, 5, 17, 8, 1, 29, 9, 9, 12, 1, + 13, 5, 40, 1, 1, 13, 5, 40, 1, 4, 6, 13, 3, 10, 6, 12, 5, 1, 5, 6, 11, 3, 11, + 6, 14, 3, 1, 5, 6, 11, 3, 11, 6, 15, 2, 1, 6, 6, 9, 3, 12, 6, 16, 1, 1, 6, 6, + 9, 3, 12, 6, 7, 1, 10, 7, 6, 7, 3, 13, 6, 6, 2, 10, 7, 6, 7, 3, 13, 14, 10, 8, + 6, 5, 3, 14, 6, 6, 2, 10, 8, 6, 5, 3, 14, 6, 7, 1, 10, 9, 6, 3, 3, 15, 6, 16, 1, + 1, 9, 6, 3, 3, 15, 6, 15, 2, 1, 10, 6, 1, 3, 16, 6, 14, 3, 1, 10, 10, 16, 6, 12, + 5, 1, 11, 8, 13, 27, 1, 11, 8, 13, 27, 1, 60] + +puts 'LOVE'.center(60) +puts 'stephan.com'.center(60) +puts "\n\n" + +puts <<~EOLOVE + A TRIBUTE TO THE GREAT AMERICAN ARTIST, ROBERT INDIANA. + HIS GREATEST WORK WILL BE REPRODUCED WITH A MESSAGE OF + YOUR CHOICE UP TO 60 CHARACTERS. IF YOU CAN'T THINK OF + A MESSAGE, SIMPLY TYPE THE WORD 'LOVE'\n +EOLOVE + +message = gets.strip +message = 'love' if message.empty? +l = message.length + +until data.empty? + puts + col = 0 + p = true + while col < 60 + run = data.shift + + if p + run.times { |i| print message[(col + i) % l] } + else + print ' ' * run + end + p = !p + col += run + end +end diff --git a/60_Mastermind/perl/README.md b/60_Mastermind/perl/README.md index e69c8b81..67786f32 100644 --- a/60_Mastermind/perl/README.md +++ b/60_Mastermind/perl/README.md @@ -1,3 +1,7 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +This is pretty much a re-implementation of the BASIC, taking advantage +of Perl's array functionality and working directly with the alphabetic +color codes. diff --git a/60_Mastermind/perl/mastermind.pl b/60_Mastermind/perl/mastermind.pl new file mode 100755 index 00000000..d27e2e47 --- /dev/null +++ b/60_Mastermind/perl/mastermind.pl @@ -0,0 +1,419 @@ +#!/usr/bin/env perl + +use 5.010; # To get 'state' and 'say' + +use strict; # Require explicit declaration of variables +use warnings; # Enable optional compiler warnings + +use English; # Use more friendly names for Perl's magic variables +use List::Util qw{ min sum }; # Convenient list utilities +use Term::ReadLine; # Prompt and return user input + +our $VERSION = '0.000_01'; + +use constant MAX_GUESSES => 10; + +print <<'EOD'; + MASTERMIND + Creative Computing Morristown, New Jersey + + +EOD + +=begin comment + + MASTERMIND II + STEVE NORTH + CREATIVE COMPUTING + PO BOX 789-M MORRISTOWN NEW JERSEY 07960 + +=end comment + +=cut + +# NOTE that mixed-case 'my' variables are 'global' in the sense that +# they are used in subroutines, but not passed to them. + +say ''; + +my $number_of_colors = get_input( + 'Number of colors [1-8]: ', + sub { m/ \A [1-8] \z /smx }, + "No more than 8, please!\n", +); + +say ''; + +my $Number_of_Positions = get_input( + 'Number of positions: ', + sub { m/ \A [0-9]+ \z /smx && $ARG }, + "A positive number, please\n", +); + +say ''; + +my $number_of_rounds = get_input( + 'Number of rounds: ', + sub { m/ \A [0-9]+ \z /smx && $ARG }, + "A positive number, please\n", +); + +my $P = $number_of_colors ** $Number_of_Positions; +say 'Total possibilities = ', $P; + +my @colors = ( qw{ + Black White Red Green Orange Yellow Purple Tan +})[ 0 .. $number_of_colors - 1 ]; +my @Color_Codes = map { uc substr $ARG, 0, 1 } @colors; + +print <<'EOD'; + + +Color Letter +===== ====== +EOD + +foreach my $inx ( 0 .. $#colors ) { + printf "%-13s%s\n", $colors[$inx], $Color_Codes[$inx]; +} + +say ''; + +my $computer_score = 0; # Computer score +my $human_score = 0; # Human score + +foreach my $round_number ( 1 .. $number_of_rounds ) { + + print <<"EOD"; + +Round number $round_number ---- + +Guess my combination. + +EOD + + $human_score += human_guesses( $Number_of_Positions ); + + print_score( $computer_score, $human_score ); + + $computer_score += computer_guesses(); + + print_score( $computer_score, $human_score ); + +} + +# Make a $pattern into a hash with one key for each possible color. The +# value for each color is the number of times it appears in the pattern. +sub hashify_pattern { + my $pattern = uc $ARG[0]; + my %p = map { $ARG => 0 } @Color_Codes; + $p{$ARG}++ for split qr//, $pattern; + return \%p; +} + +# Given a $pattern, a $guess at that pattern, and $black and $white +# scores, return a true value if the $black and $white scores of the +# $guess are those supplied as arguments; otherwise return a false +# value. This is used by computer_guesses() to eliminate possibilities. +sub analyze_black_white { + my ( $pattern, $guess, $black, $white ) = @ARG; + my $info = analyze_guess( $pattern, $guess ); + return $info->{black} == $black && $info->{white} == $white; +} + +# Given a $pattern and a $guess at that pattern, return a reference to a +# hash with the following keys: +# {guess} is the guess; +# {black} is the black score of the guess +# {white} is the white score of the guess +sub analyze_guess { + my ( $pattern, $guess ) = @ARG; + my $pattern_hash = hashify_pattern( $pattern ); + my $guess_hash = hashify_pattern( $guess ); + my $white = sum( + map { min( $pattern_hash->{$ARG}, $guess_hash->{$ARG} ) } @Color_Codes, + ); + my $black = 0; + foreach my $inx ( 0 .. length( $pattern ) - 1 ) { + if ( substr( $pattern, $inx, 1 ) eq substr( $guess, $inx, 1 ) ) + { + $black++; + --$white; + } + } + return +{ + guess => $guess, + black => $black, + white => $white, + } +} + +# Used by the computer to guess the human's choice. The return is the +# number of guesses the computer took. The return is the maximum plus +# one if the computer failed to guess. +sub computer_guesses { + + print <<'EOD'; + +Now I guess. Think of a combination. +EOD + get_input( + 'Hit when ready:', + ); + + # Generate all possible permutations. + my @possible; + foreach my $permutation ( 0 .. @Color_Codes ** $Number_of_Positions - 1 ) { + my $guess; + for ( 1 .. $Number_of_Positions ) { + my $inx = $permutation % @Color_Codes; + $guess .= $Color_Codes[ $inx ]; + $permutation = int( $permutation / @Color_Codes ); + } + push @possible, $guess; + } + + # Guess ... + foreach my $guess_num ( 1 .. MAX_GUESSES ) { + + # Guess a possible permutation at random, removing it from the + # list. + my $guess = splice @possible, int rand @possible, 1; + say 'My guess is: ', $guess; + + # Find out its black/white score. + my ( $black, $white ) = split qr< , >smx, get_input( + 'Blacks, Whites: ', + sub { m/ \A [0-9]+ , [0-9]+ \z /smx }, + "Please enter two unsigned integers\n", + ); + + # If it's all black, the computer wins. + if ( $black == $Number_of_Positions ) { + say "I got it in $guess_num moves!"; + return $guess_num; + } + + # Eliminate all possible permutations that give the black/white + # score that our guess got. If there are any left, take another + # guess. + next if @possible = grep { analyze_black_white( $ARG, $guess, $black, + $white ) } @possible; + + # There were no permutations left. Complain. + print <<'EOD'; +You have given me inconsistent information. +Try again, and this time please be more careful. +EOD + + goto &computer_guesses; # Tail-call ourselves to try again. + } + + print <<'EOD'; +I used up all my moves! +I guess my CPU is just having an off day. +EOD + + return MAX_GUESSES + 1; +} + +# Used to generate a pattern and process the human's guesses. The return +# is the number of guesses the human took. The return is the maximum +# plus one if the human failed to guess. +sub human_guesses { + + my @saved_moves; # Saved moves + my $pattern = uc join '', + map { $Color_Codes[ rand @Color_Codes ] } 1 .. $Number_of_Positions; + + foreach my $guess_num ( 1 .. MAX_GUESSES ) { + + my $guess = uc get_input( + "Move # $guess_num guess: ", + sub { + + # If the user entered 'quit', bail out. + if ( m/ \A quit \z /smxi ) { + die "Quitter! My combination was $pattern\n\nGood bye\n"; + } + + # If the user entered 'board', display the board so far. + # We return success to prevent the warning message, but + # we also clear $ARG. The caller's caller sees this and + # re-queries. + if ( m/ \A board \z /smxi ) { + print <<'EOD'; + +Board +Move Guess Black White +EOD + my $number = 1; + foreach my $item ( @saved_moves ) { + printf "%4d %-13s %3d %3d\n", $number++, + @{ $item }{ qw{ guess black white } }; + } + return undef; # Validation failure, but suppress warning. + } + + # End of special-case code. Below here we are dealing + # with guess input. + + # The length of the input must equal the number of + # positions. + if ( $Number_of_Positions != length ) { + warn "Bad number of positions\n"; + return 0; + } + + # The input may contain only valid color codes. + state $invalid_color = do { # Evaluated only once + local $LIST_SEPARATOR = ''; + qr< [^@Color_Codes] >smxi; + }; + if ( m/ ( $invalid_color ) /smxi ) { + warn "'$1' is unrecognized.\n"; + return 0; + } + + # We're good. + return 1; + }, + "Please enter 'board', 'quit', or any $Number_of_Positions of @{[ + join ', ', map { qq<'$ARG'> } @Color_Codes ]}.\n", + ); + + my $rslt = analyze_guess( $pattern, $guess ); + + push @saved_moves, $rslt; + + if ( $rslt->{black} == $Number_of_Positions ) { + say "You guessed it in $guess_num moves."; + return $guess_num; + } + + say "You have $rslt->{black} blacks and $rslt->{white} whites."; + + } + + print <<"EOD"; +You ran out of moves. That's all you get. + +The actual combination was: $pattern +EOD + + return MAX_GUESSES + 1; +} + +# Print the $computer and $human score +sub print_score { + my ( $computer, $human ) = @ARG; + print <<"EOD"; +Score: + Computer: $computer + Human: $human +EOD + return; +} + +# Get input from the user. The arguments are: +# * The prompt +# * A reference to validation code. This code receives the response in +# $ARG and returns true for a valid response. +# * A warning to print if the response is not valid. This must end in a +# return. It is suppressed if the validation code returned undef. +# The first valid response is returned. An end-of-file terminates the +# script. +sub get_input { + my ( $prompt, $validate, $warning ) = @ARG; + + # If no validator is passed, default to one that always returns + # true. + $validate ||= sub { 1 }; + + # Create the readline object. The 'state' causes the variable to be + # initialized only once, no matter how many times this subroutine is + # called. The do { ... } is a compound statement used because we + # need to tweak the created object before we store it. + state $term = do { + my $obj = Term::ReadLine->new( 'reverse' ); + $obj->ornaments( 0 ); + $obj; + }; + + while ( 1 ) { # Iterate indefinitely + + # Read the input into the topic variable, localized to prevent + # Spooky Action at a Distance. We exit on undef, which signals + # end-of-file. + exit unless defined( local $ARG = $term->readline( $prompt ) ); + + # Return the input if it is valid. + return $ARG if my $rslt = $validate->(); + + # Issue the warning, and go around the merry-go-round again. + warn $warning if defined $rslt; + } +} + +# NOTE the following is unused, but left in place in case someone wants +# to add a 'Do you want instructions?' +# +# Get a yes-or-no answer. The argument is the prompt, which will have +# '? [y/n]: ' appended. The donkey work is done by get_input(), which is +# requested to validate the response as beginning with 'y' or 'n', +# case-insensitive. The return is a true value for 'y' and a false value +# for 'n'. +sub get_yes_no { + my ( $prompt ) = @ARG; + state $map_answer = { + n => 0, + y => 1, + }; + my $resp = lc get_input( + "$prompt? [y/n]: ", + sub { m/ \A [yn] /smxi }, + "Please respond 'y' or 'n'\n", + ); + return $map_answer->{ substr $resp, 0, 1 }; +} + +__END__ + +=head1 TITLE + +mastermind - Play the game 'Mastermind' from Basic Computer Games + +=head1 SYNOPSIS + + mastermind.pl + +=head1 DETAILS + +This Perl script is a port of mastermind, which is the 60th +entry in Basic Computer Games. + +This is pretty much a re-implementation of the BASIC, taking advantage +of Perl's array functionality and working directly with the alphabetic +color codes. + +=head1 PORTED BY + +Thomas R. Wyant, III F + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2022 by Thomas R. Wyant, III + +This program is free software; you can redistribute it and/or modify it +under the same terms as Perl 5.10.0. For more details, see the Artistic +License 1.0 at +L, and/or the +Gnu GPL at L. + +This program is distributed in the hope that it will be useful, but +without any warranty; without even the implied warranty of +merchantability or fitness for a particular purpose. + +=cut + +# ex: set expandtab tabstop=4 textwidth=72 : diff --git a/76 Russian Roulette/perl/russianroulette.pl b/76 Russian Roulette/perl/russianroulette.pl deleted file mode 100644 index 0fa0cff7..00000000 --- a/76 Russian Roulette/perl/russianroulette.pl +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/perl -#use strict; -# Automatic converted by bas2perl.pl - -print ' 'x28 . "RUSSIAN ROULETTE\n"; -print ' 'x15 . "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n"; -print "\n"; print "\n"; print "\n"; -print "THIS IS A GAME OF >>>>>>>>>>RUSSIAN ROULETTE.\n"; -Line10: -print "\n"; print "HERE IS A REVOLVER.\n"; -Line20: -print "TYPE '1' TO SPIN CHAMBER AND PULL TRIGGER.\n"; -print "TYPE '2' TO GIVE UP.\n"; -print "GO"; -$N=0; -Line30: -print "? "; chomp($I = ); -if ($I ne 2) { goto Line35; } -print " CHICKEN!!!!!\n"; -goto Line72; -Line35: -$N=$N+1; -if (rand(1)>.833333) { goto Line70; } -if ($N>10) { goto Line80; } -print "- CLICK -\n"; -print "\n"; goto Line30; -Line70: -print " BANG!!!!! YOU'RE DEAD!\n"; -print "CONDOLENCES WILL BE SENT TO YOUR RELATIVES.\n"; -Line72: -print "\n"; print "\n"; print "\n"; -print "...NEXT VICTIM...\n"; goto Line20; -Line80: -print "YOU WIN!!!!!\n"; -print "LET SOMEONE ELSE BLOW HIS BRAINS OUT.\n"; -goto Line10; -exit; - - diff --git a/83_Stock_Market/java/StockMarket.java b/83_Stock_Market/java/StockMarket.java new file mode 100644 index 00000000..142f6fb6 --- /dev/null +++ b/83_Stock_Market/java/StockMarket.java @@ -0,0 +1,426 @@ +import java.util.ArrayList; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Random; +import java.util.Scanner; + +/** + * Stock Market Simulation + * + * Some of the original program's variables' documentation and their equivalent in this program: + * A-MRKT TRND SLP; marketTrendSlope + * B5-BRKRGE FEE; brokerageFee + * C-TTL CSH ASSTS; cashAssets + * C5-TTL CSH ASSTS (TEMP); tmpCashAssets + * C(I)-CHNG IN STK VAL; changeStockValue + * D-TTL ASSTS; assets + * E1,E2-LRG CHNG MISC; largeChange1, largeChange2 + * I1,I2-STCKS W LRG CHNG; randomStockIndex1, randomStockIndex2 + * N1,N2-LRG CHNG DAY CNTS; largeChangeNumberDays1, largeChangeNumberDays2 + * P5-TTL DAYS PRCHSS; totalDaysPurchases + * P(I)-PRTFL CNTNTS; portfolioContents + * Q9-NEW CYCL?; newCycle + * S4-SGN OF A; slopeSign + * S5-TTL DYS SLS; totalDaysSales + * S(I)-VALUE/SHR; stockValue + * T-TTL STCK ASSTS; totalStockAssets + * T5-TTL VAL OF TRNSCTNS; totalValueOfTransactions + * W3-LRG CHNG; bigChange + * X1-SMLL CHNG(<$1); smallChange + * Z4,Z5,Z6-NYSE AVE.; tmpNyseAverage, nyseAverage, nyseAverageChange + * Z(I)-TRNSCT transactionQuantity + * + * new price = old price + (trend x old price) + (small random price + * change) + (possible large price change) + * + * Converted from BASIC to Java by Aldrin Misquitta (@aldrinm) + */ +public class StockMarket { + + private static final Random random = new Random(); + + public static void main(String[] args) { + + Scanner scan = new Scanner(System.in); + + printIntro(); + printGameHelp(scan); + + final List stocks = initStocks(); + + double marketTrendSlope = Math.floor((random.nextFloat() / 10) * 100 + 0.5)/100f; + double totalValueOfTransactions; + int largeChangeNumberDays1 = 0; + int largeChangeNumberDays2 = 0; + + //DAYS FOR FIRST TREND SLOPE (A) + var t8 = randomNumber(1, 6); + + //RANDOMIZE SIGN OF FIRST TREND SLOPE (A) + if (random.nextFloat() <= 0.5) { + marketTrendSlope = -marketTrendSlope; + } + + // INITIALIZE CASH ASSETS:C + double cashAssets = 10000; + boolean largeChange1 = false; + boolean largeChange2 = false; + double tmpNyseAverage; + double nyseAverage = 0; + boolean inProgress = true; + var firstRound = true; + + while (inProgress) { + + /* Original documentation: + RANDOMLY PRODUCE NEW STOCK VALUES BASED ON PREVIOUS DAY'S VALUES + N1,N2 ARE RANDOM NUMBERS OF DAYS WHICH RESPECTIVELY + DETERMINE WHEN STOCK I1 WILL INCREASE 10 PTS. AND STOCK + I2 WILL DECREASE 10 PTS. + IF N1 DAYS HAVE PASSED, PICK AN I1, SET E1, DETERMINE NEW N1 + */ + int randomStockIndex1 = 0; + int randomStockIndex2 = 0; + + if (largeChangeNumberDays1 <= 0) { + randomStockIndex1 = randomNumber(0, stocks.size()); + largeChangeNumberDays1 = randomNumber(1, 6); + largeChange1 = true; + } + if (largeChangeNumberDays2 <= 0) { + randomStockIndex2 = randomNumber(0, stocks.size()); + largeChangeNumberDays2 = randomNumber(1, 6); + largeChange2 = true; + } + adjustAllStockValues(stocks, largeChange1, largeChange2, marketTrendSlope, stocks.get(randomStockIndex1), stocks.get(randomStockIndex2)); + + //reset largeChange flags + largeChange1 = false; + largeChange2 = false; + largeChangeNumberDays1--; + largeChangeNumberDays2--; + + //AFTER T8 DAYS RANDOMLY CHANGE TREND SIGN AND SLOPE + t8 = t8 - 1; + if (t8 < 1) { + marketTrendSlope = newMarketTrendSlope(); + t8 = randomNumber(1, 6); + } + + //PRINT PORTFOLIO + printPortfolio(firstRound, stocks); + + tmpNyseAverage = nyseAverage; + nyseAverage = 0; + double totalStockAssets = 0; + for (Stock stock : stocks) { + nyseAverage = nyseAverage + stock.getStockValue(); + totalStockAssets = totalStockAssets + stock.getStockValue() * stock.getPortfolioContents(); + } + nyseAverage = Math.floor(100 * (nyseAverage / 5) + .5) / 100f; + double nyseAverageChange = Math.floor((nyseAverage - tmpNyseAverage) * 100 + .5) / 100f; + + // TOTAL ASSETS:D + double assets = totalStockAssets + cashAssets; + if (firstRound) { + System.out.printf("\n\nNEW YORK STOCK EXCHANGE AVERAGE: %.2f", nyseAverage); + } else { + System.out.printf("\n\nNEW YORK STOCK EXCHANGE AVERAGE: %.2f NET CHANGE %.2f", nyseAverage, nyseAverageChange); + } + + totalStockAssets = Math.floor(100 * totalStockAssets + 0.5) / 100d; + System.out.printf("\n\nTOTAL STOCK ASSETS ARE $ %.2f", totalStockAssets); + cashAssets = Math.floor(100 * cashAssets + 0.5) / 100d; + System.out.printf("\nTOTAL CASH ASSETS ARE $ %.2f", cashAssets); + assets = Math.floor(100 * assets + .5) / 100d; + System.out.printf("\nTOTAL ASSETS ARE $ %.2f\n", assets); + + if (!firstRound) { + System.out.print("\nDO YOU WISH TO CONTINUE (YES-TYPE 1, NO-TYPE 0)? "); + var newCycle = readANumber(scan); + if (newCycle < 1) { + System.out.println("HOPE YOU HAD FUN!!"); + inProgress = false; + } + } + + if (inProgress) { + boolean validTransaction = false; + // TOTAL DAY'S PURCHASES IN $:P5 + double totalDaysPurchases = 0; + // TOTAL DAY'S SALES IN $:S5 + double totalDaysSales = 0; + double tmpCashAssets; + while (!validTransaction) { + //INPUT TRANSACTIONS + readStockTransactions(stocks, scan); + totalDaysPurchases = 0; + totalDaysSales = 0; + + validTransaction = true; + for (Stock stock : stocks) { + stock.setTransactionQuantity(Math.floor(stock.getTransactionQuantity() + 0.5)); + if (stock.getTransactionQuantity() > 0) { + totalDaysPurchases = totalDaysPurchases + stock.getTransactionQuantity() * stock.getStockValue(); + } else { + totalDaysSales = totalDaysSales - stock.getTransactionQuantity() * stock.getStockValue(); + if (-stock.getTransactionQuantity() > stock.getPortfolioContents()) { + System.out.println("YOU HAVE OVERSOLD A STOCK; TRY AGAIN."); + validTransaction = false; + break; + } + } + } + + //TOTAL VALUE OF TRANSACTIONS:T5 + totalValueOfTransactions = totalDaysPurchases + totalDaysSales; + // BROKERAGE FEE:B5 + var brokerageFee = Math.floor(0.01 * totalValueOfTransactions * 100 + .5) / 100d; + // CASH ASSETS=OLD CASH ASSETS-TOTAL PURCHASES + //-BROKERAGE FEES+TOTAL SALES:C5 + tmpCashAssets = cashAssets - totalDaysPurchases - brokerageFee + totalDaysSales; + if (tmpCashAssets < 0) { + System.out.printf("\nYOU HAVE USED $%.2f MORE THAN YOU HAVE.", -tmpCashAssets); + validTransaction = false; + } else { + cashAssets = tmpCashAssets; + } + } + + // CALCULATE NEW PORTFOLIO + for (Stock stock : stocks) { + stock.setPortfolioContents(stock.getPortfolioContents() + stock.getTransactionQuantity()); + } + + firstRound = false; + } + + } + } + + /** + * Random int between lowerBound(inclusive) and upperBound(exclusive) + */ + private static int randomNumber(int lowerBound, int upperBound) { + return random.nextInt((upperBound - lowerBound)) + lowerBound; + } + + private static double newMarketTrendSlope() { + return randomlyChangeTrendSignAndSlopeAndDuration(); + } + + private static void printPortfolio(boolean firstRound, List stocks) { + //BELL RINGING-DIFFERENT ON MANY COMPUTERS + if (firstRound) { + System.out.printf("%n%-30s\t%12s\t%12s", "STOCK", "INITIALS", "PRICE/SHARE"); + for (Stock stock : stocks) { + System.out.printf("%n%-30s\t%12s\t%12.2f ------ %12.2f", stock.getStockName(), stock.getStockCode(), + stock.getStockValue(), stock.getChangeStockValue()); + } + System.out.println(""); + } else { + System.out.println("\n********** END OF DAY'S TRADING **********\n\n"); + System.out.printf("%n%-12s\t%-12s\t%-12s\t%-12s\t%-20s", "STOCK", "PRICE/SHARE", + "HOLDINGS", "VALUE", "NET PRICE CHANGE"); + for (Stock stock : stocks) { + System.out.printf("%n%-12s\t%-12.2f\t%-12.0f\t%-12.2f\t%-20.2f", + stock.getStockCode(), stock.getStockValue(), stock.getPortfolioContents(), + stock.getStockValue() * stock.getPortfolioContents(), stock.getChangeStockValue()); + } + } + } + + private static void readStockTransactions(List stocks, Scanner scan) { + System.out.println("\n\nWHAT IS YOUR TRANSACTION IN"); + for (Stock stock : stocks) { + System.out.printf("%s? ", stock.getStockCode()); + + stock.setTransactionQuantity(readANumber(scan)); + } + } + + private static int readANumber(Scanner scan) { + int choice = 0; + + boolean validInput = false; + while (!validInput) { + try { + choice = scan.nextInt(); + validInput = true; + } catch (InputMismatchException ex) { + System.out.println("!NUMBER EXPECTED - RETRY INPUT LINE"); + } finally { + scan.nextLine(); + } + } + + return choice; + } + + private static void adjustAllStockValues(List stocks, boolean largeChange1, + boolean largeChange2, + double marketTrendSlope, + Stock stockForLargeChange1, Stock stockForLargeChange2 + ) { + //LOOP THROUGH ALL STOCKS + for (Stock stock : stocks) { + double smallChange = random.nextFloat(); + + if (smallChange <= 0.25) { + smallChange = 0.25; + } else if (smallChange <= 0.5) { + smallChange = 0.5; + } else if (smallChange <= 0.75) { + smallChange = 0.75; + } else { + smallChange = 0; + } + + //BIG CHANGE CONSTANT:W3 (SET TO ZERO INITIALLY) + var bigChange = 0; + if (largeChange1) { + if (stock.getStockCode().equals(stockForLargeChange1.getStockCode())) { + //ADD 10 PTS. TO THIS STOCK; RESET E1 + bigChange = 10; + } + } + + if (largeChange2) { + if (stock.getStockCode().equals(stockForLargeChange2.getStockCode())) { + //SUBTRACT 10 PTS. FROM THIS STOCK; RESET E2 + bigChange = bigChange - 10; + } + } + + stock.setChangeStockValue(Math.floor(marketTrendSlope * stock.stockValue) + smallChange + + Math.floor(3 - 6 * random.nextFloat() + .5) + bigChange); + stock.setChangeStockValue(Math.floor(100 * stock.getChangeStockValue() + .5) / 100d); + stock.stockValue += stock.getChangeStockValue(); + + if (stock.stockValue > 0) { + stock.stockValue = Math.floor(100 * stock.stockValue + 0.5) / 100d; + } else { + stock.setChangeStockValue(0); + stock.stockValue = 0; + } + } + } + + private static double randomlyChangeTrendSignAndSlopeAndDuration() { + // RANDOMLY CHANGE TREND SIGN AND SLOPE (A), AND DURATION + var newTrend = Math.floor((random.nextFloat() / 10) * 100 + .5) / 100d; + var slopeSign = random.nextFloat(); + if (slopeSign > 0.5) { + newTrend = -newTrend; + } + return newTrend; + } + + private static List initStocks() { + List stocks = new ArrayList<>(); + stocks.add(new Stock(100, "INT. BALLISTIC MISSILES", "IBM")); + stocks.add(new Stock(85, "RED CROSS OF AMERICA", "RCA")); + stocks.add(new Stock(150, "LICHTENSTEIN, BUMRAP & JOKE", "LBJ")); + stocks.add(new Stock(140, "AMERICAN BANKRUPT CO.", "ABC")); + stocks.add(new Stock(110, "CENSURED BOOKS STORE", "CBS")); + return stocks; + } + + private static void printGameHelp(Scanner scan) { + System.out.print("DO YOU WANT THE INSTRUCTIONS (YES-TYPE 1, NO-TYPE 0) ? "); + int choice = scan.nextInt(); + if (choice >= 1) { + System.out.println(""); + System.out.println("THIS PROGRAM PLAYS THE STOCK MARKET. YOU WILL BE GIVEN"); + System.out.println("$10,000 AND MAY BUY OR SELL STOCKS. THE STOCK PRICES WILL"); + System.out.println("BE GENERATED RANDOMLY AND THEREFORE THIS MODEL DOES NOT"); + System.out.println("REPRESENT EXACTLY WHAT HAPPENS ON THE EXCHANGE. A TABLE"); + System.out.println("OF AVAILABLE STOCKS, THEIR PRICES, AND THE NUMBER OF SHARES"); + System.out.println("IN YOUR PORTFOLIO WILL BE PRINTED. FOLLOWING THIS, THE"); + System.out.println("INITIALS OF EACH STOCK WILL BE PRINTED WITH A QUESTION"); + System.out.println("MARK. HERE YOU INDICATE A TRANSACTION. TO BUY A STOCK"); + System.out.println("TYPE +NNN, TO SELL A STOCK TYPE -NNN, WHERE NNN IS THE"); + System.out.println("NUMBER OF SHARES. A BROKERAGE FEE OF 1% WILL BE CHARGED"); + System.out.println("ON ALL TRANSACTIONS. NOTE THAT IF A STOCK'S VALUE DROPS"); + System.out.println("TO ZERO IT MAY REBOUND TO A POSITIVE VALUE AGAIN. YOU"); + System.out.println("HAVE $10,000 TO INVEST. USE INTEGERS FOR ALL YOUR INPUTS."); + System.out.println("(NOTE: TO GET A 'FEEL' FOR THE MARKET RUN FOR AT LEAST"); + System.out.println("10 DAYS)"); + System.out.println("-----GOOD LUCK!-----"); + } + System.out.println("\n\n"); + } + + private static void printIntro() { + System.out.println(" STOCK MARKET"); + System.out.println(" CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"); + System.out.println("\n\n"); + } + + /** + * Stock class also storing the stock information and other related information for simplicity + */ + private static class Stock { + + private final String stockName; + private final String stockCode; + private double stockValue; + private double portfolioContents = 0; + private double transactionQuantity = 0; + private double changeStockValue = 0; + + public Stock(double stockValue, String stockName, String stockCode) { + this.stockValue = stockValue; + this.stockName = stockName; + this.stockCode = stockCode; + } + + public String getStockName() { + return stockName; + } + + public String getStockCode() { + return stockCode; + } + + public double getStockValue() { + return stockValue; + } + + public double getPortfolioContents() { + return portfolioContents; + } + + public void setPortfolioContents(double portfolioContents) { + this.portfolioContents = portfolioContents; + } + + public double getTransactionQuantity() { + return transactionQuantity; + } + + public void setTransactionQuantity(double transactionQuantity) { + this.transactionQuantity = transactionQuantity; + } + + public double getChangeStockValue() { + return changeStockValue; + } + + public void setChangeStockValue(double changeStockValue) { + this.changeStockValue = changeStockValue; + } + + @Override + public String toString() { + return "Stock{" + + "stockValue=" + stockValue + + ", stockCode='" + stockCode + '\'' + + ", portfolioContents=" + portfolioContents + + ", transactionQuantity=" + transactionQuantity + + ", changeStockValue=" + changeStockValue + + '}'; + } + } + +} \ No newline at end of file diff --git a/89_Tic-Tac-Toe/perl/tictactoe2.pl b/89_Tic-Tac-Toe/perl/tictactoe2.pl index 0cbf4c10..4bc7cc0b 100644 --- a/89_Tic-Tac-Toe/perl/tictactoe2.pl +++ b/89_Tic-Tac-Toe/perl/tictactoe2.pl @@ -110,7 +110,7 @@ sub check_for_corners { @precedence=(1,9,7,3,5,2,4,6,8); } else { - @precedence=(5,1,9,7,3,2,4,6,8); + @precedence=(5,2,4,6,8,1,9,7,3); } foreach my $move (@precedence) { my $validity=&check_occupation($move); @@ -166,7 +166,7 @@ sub check_occupation { sub print_board { foreach my $num (1..9) { - my $char = &which_char($board{$num}); + my $char = &which_char($board{$num}); if ($num == 4 || $num == 7) { print "\n---+---+---\n";} print "$char"; if ($num % 3 > 0) { print "!" } diff --git a/94_War/javascript/war.js b/94_War/javascript/war.js index ededa1ed..ca11d063 100644 --- a/94_War/javascript/war.js +++ b/94_War/javascript/war.js @@ -39,11 +39,11 @@ function input() { async function askYesOrNo(question) { while (1) { print(question); - const str = await input(); - if (str == "YES") { + const str = (await input()).toUpperCase(); + if (str === "YES") { return true; } - else if (str == "NO") { + else if (str === "NO") { return false; } else { diff --git a/94_War/ruby/war.rb b/94_War/ruby/war.rb new file mode 100644 index 00000000..cf7e8a59 --- /dev/null +++ b/94_War/ruby/war.rb @@ -0,0 +1,118 @@ +#!/usr/bin/env ruby +# reinterpreted from BASIC by stephan.com +class War + class Card + class CardError < StandardError; end + + SUITS = %i[spades hearts clubs diamonds].freeze + PIPS = %i[ace deuce trey four five six seven eight nine ten jack king queen].freeze + CARDS = SUITS.product(PIPS).freeze + VALUES = PIPS.zip(1..13).to_h.freeze + + attr_reader :value + + def initialize(suit, pip) + @suit = suit + @pip = pip + raise CardError, 'invalid suit' unless SUITS.include? @suit + raise CardError, 'invalid pip' unless PIPS.include? @pip + + @value = VALUES[pip] + end + + def <=>(other) + @value <=> other.value + end + + def >(other) + @value > other.value + end + + def <(other) + @value < other.value + end + + def to_s + "the #{@pip} of #{@suit}" + end + + def self.shuffle + CARDS.map { |suit, pip| new(suit, pip) }.shuffle + end + end + + def initialize + @your_score = 0 + @computer_score = 0 + @your_deck = Card.shuffle + @computer_deck = Card.shuffle + end + + def play + intro + + loop do + puts "\nYou: #{@your_score} Computer: #{@computer_score}" + round @your_deck.shift, @computer_deck.shift + break if empty? + + puts 'Do you want to continue?' + break unless yesno + end + + outro + end + + private + + def round(your_card, computer_card) + puts "You: #{your_card} vs Computer: #{computer_card}" + return puts 'Tie. No score change.' if your_card == computer_card + + if computer_card > your_card + puts "Computer wins with #{computer_card}" + @computer_score += 1 + else + puts "You win with #{your_card}" + @your_score += 1 + end + end + + def yesno + loop do + wants = gets.strip + return true if wants.downcase == 'yes' + return false if wants.downcase == 'no' + + puts 'Yes or no, please.' + end + end + + def intro + puts 'War'.center(80) + puts 'stephan.com'.center(80) + puts + puts 'This is the card game of war.' + puts 'Do you want directions' + directions if yesno + end + + def directions + puts 'The computer gives you and it a \'card\'. The higher card' + puts '(numerically) wins. The game ends when you choose not to' + puts 'continue or when you have finished the pack.' + puts + end + + def outro + puts "We've run out of cards" if empty? + puts "Final score:\nYou: #{@your_score}\nComputer: #{@computer_score}" + puts 'Thanks for playing!' + end + + def empty? + @your_deck.empty? || @computer_deck.empty? + end +end + +War.new.play diff --git a/95_Weekday/javascript/weekday.js b/95_Weekday/javascript/weekday.js index 276e1817..6070c71c 100644 --- a/95_Weekday/javascript/weekday.js +++ b/95_Weekday/javascript/weekday.js @@ -3,83 +3,345 @@ // Converted from BASIC to Javascript by Oscar Toledo G. (nanochess) // -function print(str) -{ +/** + * Print given string to the end of the "output" element. + * @param str + */ +function print(str) { document.getElementById("output").appendChild(document.createTextNode(str)); } -function input() -{ - var input_element; - var input_str; - +/** + * Obtain user input + * @returns {Promise} + */ +function input() { return new Promise(function (resolve) { - input_element = document.createElement("INPUT"); - - print("? "); - input_element.setAttribute("type", "text"); - input_element.setAttribute("length", "50"); - document.getElementById("output").appendChild(input_element); - input_element.focus(); - input_str = undefined; - input_element.addEventListener("keydown", function (event) { - if (event.keyCode == 13) { - input_str = input_element.value; - document.getElementById("output").removeChild(input_element); - print(input_str); - print("\n"); - resolve(input_str); - } - }); - }); + const input_element = document.createElement("INPUT"); + + print("? "); + input_element.setAttribute("type", "text"); + input_element.setAttribute("length", "50"); + document.getElementById("output").appendChild(input_element); + input_element.focus(); + input_element.addEventListener("keydown", function (event) { + if (event.keyCode === 13) { + const input_str = input_element.value; + document.getElementById("output").removeChild(input_element); + print(input_str); + print("\n"); + resolve(input_str); + } + }); + }); } -function tab(space) -{ - var str = ""; - while (space-- > 0) +/** + * Create a string consisting of the given number of spaces + * @param spaceCount + * @returns {string} + */ +function tab(spaceCount) { + let str = ""; + while (spaceCount-- > 0) str += " "; return str; } -function fna(arg) { - return Math.floor(arg / 4); +const MONTHS_PER_YEAR = 12; +const DAYS_PER_COMMON_YEAR = 365; +const DAYS_PER_IDEALISED_MONTH = 30; +const MAXIMUM_DAYS_PER_MONTH = 31; +// In a common (non-leap) year the day of the week for the first of each month moves by the following amounts. +const COMMON_YEAR_MONTH_OFFSET = [0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5]; + +/** + * Date representation. + */ +class DateStruct { + #year; + #month; + #day; + + /** + * Build a DateStruct + * @param {number} year + * @param {number} month + * @param {number} day + */ + constructor(year, month, day) { + this.#year = year; + this.#month = month; + this.#day = day; + } + + get year() { + return this.#year; + } + + get month() { + return this.#month; + } + + get day() { + return this.#day; + } + + /** + * Determine if the date could be a Gregorian date. + * Be aware the Gregorian calendar was not introduced in all places at once, + * see https://en.wikipedia.org/wiki/Gregorian_calendar + * @returns {boolean} true if date could be Gregorian; otherwise false. + */ + isGregorianDate() { + let result = false; + if (this.#year > 1582) { + result = true; + } else if (this.#year === 1582) { + if (this.#month > 10) { + result = true; + } else if (this.#month === 10 && this.#day >= 15) { + result = true; + } + } + return result; + } + + /** + * The following performs a hash on the day parts which guarantees that + * 1. different days will return different numbers + * 2. the numbers returned are ordered. + * @returns {number} + */ + getNormalisedDay() { + return (this.year * MONTHS_PER_YEAR + this.month) * MAXIMUM_DAYS_PER_MONTH + this.day; + } + + /** + * Determine the day of the week. + * This calculation returns a number between 1 and 7 where Sunday=1, Monday=2, ..., Saturday=7. + * @returns {number} Value between 1 and 7 representing Sunday to Saturday. + */ + getDayOfWeek() { + // Calculate an offset based on the century part of the year. + const centuriesSince1500 = Math.floor((this.year - 1500) / 100); + let centuryOffset = centuriesSince1500 * 5 + (centuriesSince1500 + 3) / 4; + centuryOffset = Math.floor(centuryOffset % 7); + + // Calculate an offset based on the shortened two digit year. + // January 1st moves forward by approximately 1.25 days per year + const yearInCentury = this.year % 100; + const yearInCenturyOffsets = yearInCentury / 4 + yearInCentury; + + // combine offsets with day and month + let dayOfWeek = centuryOffset + yearInCenturyOffsets + this.day + COMMON_YEAR_MONTH_OFFSET[this.month - 1]; + + dayOfWeek = Math.floor(dayOfWeek % 7) + 1; + if (this.month <= 2 && this.isLeapYear()) { + dayOfWeek--; + } + if (dayOfWeek === 0) { + dayOfWeek = 7; + } + return dayOfWeek; + } + + /** + * Determine if the given year is a leap year. + * @returns {boolean} + */ + isLeapYear() { + if ((this.year % 4) !== 0) { + return false; + } else if ((this.year % 100) !== 0) { + return true; + } else if ((this.year % 400) !== 0) { + return false; + } + return true; + } + + /** + * Returns a US formatted date, i.e. Month/Day/Year. + * @returns {string} + */ + toString() { + return this.#month + "/" + this.#day + "/" + this.#year; + } } -function fnb(arg) { - return Math.floor(arg / 7); -} +/** + * Duration representation. + * Note: this class only handles positive durations well + */ +class Duration { + #years; + #months; + #days; -var t = [, 0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5]; - -var k5; -var k6; -var k7; - -function time_spent(f, a8) -{ - k1 = Math.floor(f * a8); - i5 = Math.floor(k1 / 365); - k1 -= i5 * 365; - i6 = Math.floor(k1 / 30); - i7 = k1 - (i6 * 30); - k5 -= i5; - k6 -= i6; - k7 -= i7; - if (k7 < 0) { - k7 += 30; - k6--; + /** + * Build a Duration + * @param {number} years + * @param {number} months + * @param {number} days + */ + constructor(years, months, days) { + this.#years = years; + this.#months = months; + this.#days = days; + this.#fixRanges(); } - if (k6 <= 0) { - k6 += 12; - k5--; + + get years() { + return this.#years; + } + + get months() { + return this.#months; + } + + get days() { + return this.#days; + } + + clone() { + return new Duration(this.#years, this.#months, this.#days); + } + + /** + * Adjust Duration by removing years, months and days from supplied Duration. + * This is a naive calculation which assumes all months are 30 days. + * @param {Duration} timeToRemove + */ + remove(timeToRemove) { + this.#years -= timeToRemove.years; + this.#months -= timeToRemove.months; + this.#days -= timeToRemove.days; + this.#fixRanges(); + } + + /** + * Move days and months into expected range. + */ + #fixRanges() { + if (this.#days < 0) { + this.#days += DAYS_PER_IDEALISED_MONTH; + this.#months--; + } + if (this.#months < 0) { + this.#months += MONTHS_PER_YEAR; + this.#years--; + } + } + + /** + * Computes an approximation of the days covered by the duration. + * The calculation assumes all years are 365 days, months are 30 days each, + * and adds on an extra bit the more months that have passed. + * @returns {number} + */ + getApproximateDays() { + return ( + (this.#years * DAYS_PER_COMMON_YEAR) + + (this.#months * DAYS_PER_IDEALISED_MONTH) + + this.#days + + Math.floor(this.#months / 2) + ); + } + + /** + * Returns a formatted duration with tab separated values, i.e. Years\tMonths\tDays. + * @returns {string} + */ + toString() { + return this.#years + "\t" + this.#months + "\t" + this.#days; + } + + /** + * Determine approximate Duration between two dates. + * This is a naive calculation which assumes all months are 30 days. + * @param {DateStruct} date1 + * @param {DateStruct} date2 + * @returns {Duration} + */ + static between(date1, date2) { + let years = date1.year - date2.year; + let months = date1.month - date2.month; + let days = date1.day - date2.day; + return new Duration(years, months, days); + } + + /** + * Calculate years, months and days as factor of days. + * This is a naive calculation which assumes all months are 30 days. + * @param dayCount Total day to convert to a duration + * @param factor Factor to apply when calculating the duration + * @returns {Duration} + */ + static fromDays(dayCount, factor) { + let totalDays = Math.floor(factor * dayCount); + const years = Math.floor(totalDays / DAYS_PER_COMMON_YEAR); + totalDays -= years * DAYS_PER_COMMON_YEAR; + const months = Math.floor(totalDays / DAYS_PER_IDEALISED_MONTH); + const days = totalDays - (months * DAYS_PER_IDEALISED_MONTH); + return new Duration(years, months, days); } - print(i5 + "\t" + i6 + "\t" + i7 + "\n"); } // Main control section -async function main() -{ +async function main() { + /** + * Reads a date, and extracts the date information. + * This expects date parts to be comma separated, using US date ordering, + * i.e. Month,Day,Year. + * @returns {Promise} + */ + async function inputDate() { + let dateString = await input(); + const month = parseInt(dateString); + const day = parseInt(dateString.substr(dateString.indexOf(",") + 1)); + const year = parseInt(dateString.substr(dateString.lastIndexOf(",") + 1)); + return new DateStruct(year, month, day); + } + + /** + * Obtain text for the day of the week. + * @param {DateStruct} date + * @returns {string} + */ + function getDayOfWeekText(date) { + const dayOfWeek = date.getDayOfWeek(); + let dayOfWeekText = ""; + switch (dayOfWeek) { + case 1: + dayOfWeekText = "SUNDAY."; + break; + case 2: + dayOfWeekText = "MONDAY."; + break; + case 3: + dayOfWeekText = "TUESDAY."; + break; + case 4: + dayOfWeekText = "WEDNESDAY."; + break; + case 5: + dayOfWeekText = "THURSDAY."; + break; + case 6: + if (date.day === 13) { + dayOfWeekText = "FRIDAY THE THIRTEENTH---BEWARE!"; + } else { + dayOfWeekText = "FRIDAY."; + } + break; + case 7: + dayOfWeekText = "SATURDAY."; + break; + } + return dayOfWeekText; + } + print(tab(32) + "WEEKDAY\n"); print(tab(15) + "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n"); print("\n"); @@ -89,111 +351,69 @@ async function main() print("GIVES FACTS ABOUT A DATE OF INTEREST TO YOU.\n"); print("\n"); print("ENTER TODAY'S DATE IN THE FORM: 3,24,1979 "); - str = await input(); - m1 = parseInt(str); - d1 = parseInt(str.substr(str.indexOf(",") + 1)); - y1 = parseInt(str.substr(str.lastIndexOf(",") + 1)); + const today = await inputDate(); // This program determines the day of the week // for a date after 1582 print("ENTER DAY OF BIRTH (OR OTHER DAY OF INTEREST)"); - str = await input(); - m = parseInt(str); - d = parseInt(str.substr(str.indexOf(",") + 1)); - y = parseInt(str.substr(str.lastIndexOf(",") + 1)); + const dateOfBirth = await inputDate(); print("\n"); - i1 = Math.floor((y - 1500) / 100); // Test for date before current calendar. - if (y - 1582 < 0) { - print("NOT PREPARED TO GIVE DAY OF WEEK PRIOR TO MDLXXXII.\n"); + if (!dateOfBirth.isGregorianDate()) { + print("NOT PREPARED TO GIVE DAY OF WEEK PRIOR TO X.XV.MDLXXXII.\n"); } else { - a = i1 * 5 + (i1 + 3) / 4; - i2 = Math.floor(a - fnb(a) * 7); - y2 = Math.floor(y / 100); - y3 = Math.floor(y - y2 * 100); - a = y3 / 4 + y3 + d + t[m] + i2; - b = Math.floor(a - fnb(a) * 7) + 1; - if (m <= 2) { - if (y3 != 0) { - t1 = Math.floor(y - fna(y) * 4); - } else { - a = i1 - 1; - t1 = Math.floor(a - fna(a) * 4); - } - if (t1 == 0) { - if (b == 0) - b = 6; - b--; - } - } - if (b == 0) - b = 7; - if ((y1 * 12 + m1) * 31 + d1 < (y * 12 + m) * 31 + d) { - print(m + "/" + d + "/" + y + " WILL BE A "); - } else if ((y1 * 12 + m1) * 31 + d1 == (y * 12 + m) * 31 + d) { - print(m + "/" + d + "/" + y + " IS A "); + const normalisedToday = today.getNormalisedDay(); + const normalisedDob = dateOfBirth.getNormalisedDay(); + + let dayOfWeekText = getDayOfWeekText(dateOfBirth); + if (normalisedToday < normalisedDob) { + print(dateOfBirth + " WILL BE A " + dayOfWeekText + "\n"); + } else if (normalisedToday === normalisedDob) { + print(dateOfBirth + " IS A " + dayOfWeekText + "\n"); } else { - print(m + "/" + d + "/" + y + " WAS A "); + print(dateOfBirth + " WAS A " + dayOfWeekText + "\n"); } - switch (b) { - case 1: print("SUNDAY.\n"); break; - case 2: print("MONDAY.\n"); break; - case 3: print("TUESDAY.\n"); break; - case 4: print("WEDNESDAY.\n"); break; - case 5: print("THURSDAY.\n"); break; - case 6: - if (d == 13) { - print("FRIDAY THE THIRTEENTH---BEWARE!\n"); - } else { - print("FRIDAY.\n"); - } - break; - case 7: print("SATURDAY.\n"); break; - } - if ((y1 * 12 + m1) * 31 + d1 != (y * 12 + m) * 31 + d) { - i5 = y1 - y; + + if (normalisedToday !== normalisedDob) { print("\n"); - i6 = m1 - m; - i7 = d1 - d; - if (i7 < 0) { - i6--; - i7 += 30; - } - if (i6 < 0) { - i5--; - i6 += 12; - } - if (i5 >= 0) { - if (i7 == 0 && i6 == 0) + let differenceBetweenDates = Duration.between(today, dateOfBirth); + if (differenceBetweenDates.years >= 0) { + if (differenceBetweenDates.days === 0 && differenceBetweenDates.months === 0) { print("***HAPPY BIRTHDAY***\n"); + } print(" \tYEARS\tMONTHS\tDAYS\n"); print(" \t-----\t------\t----\n"); - print("YOUR AGE (IF BIRTHDATE) \t" + i5 + "\t" + i6 + "\t" + i7 + "\n"); - a8 = (i5 * 365) + (i6 * 30) + i7 + Math.floor(i6 / 2); - k5 = i5; - k6 = i6; - k7 = i7; - // Calculate retirement date. - e = y + 65; - // Calculate time spent in the following functions. - print("YOU HAVE SLEPT \t\t\t"); - time_spent(0.35, a8); - print("YOU HAVE EATEN \t\t\t"); - time_spent(0.17, a8); - if (k5 <= 3) { - print("YOU HAVE PLAYED \t\t\t"); - } else if (k5 <= 9) { - print("YOU HAVE PLAYED/STUDIED \t\t"); + print("YOUR AGE (IF BIRTHDATE) \t" + differenceBetweenDates + "\n"); + + const approximateDaysBetween = differenceBetweenDates.getApproximateDays(); + const unaccountedTime = differenceBetweenDates.clone(); + + // 35% sleeping + const sleepTimeSpent = Duration.fromDays(approximateDaysBetween, 0.35); + print("YOU HAVE SLEPT \t\t\t" + sleepTimeSpent + "\n"); + unaccountedTime.remove(sleepTimeSpent); + + // 17% eating + const eatenTimeSpent = Duration.fromDays(approximateDaysBetween, 0.17); + print("YOU HAVE EATEN \t\t\t" + eatenTimeSpent + "\n"); + unaccountedTime.remove(eatenTimeSpent); + + // 23% working, studying or playing + const workPlayTimeSpent = Duration.fromDays(approximateDaysBetween, 0.23); + if (unaccountedTime.years <= 3) { + print("YOU HAVE PLAYED \t\t" + workPlayTimeSpent + "\n"); + } else if (unaccountedTime.years <= 9) { + print("YOU HAVE PLAYED/STUDIED \t" + workPlayTimeSpent + "\n"); } else { - print("YOU HAVE WORKED/PLAYED \t\t"); + print("YOU HAVE WORKED/PLAYED \t\t" + workPlayTimeSpent + "\n"); } - time_spent(0.23, a8); - if (k6 == 12) { - k5++; - k6 = 0; - } - print("YOU HAVE RELAXED \t\t" + k5 + "\t" + k6 + "\t" + k7 + "\n"); + unaccountedTime.remove(workPlayTimeSpent); + + // Remaining time spent relaxing + print("YOU HAVE RELAXED \t\t" + unaccountedTime + "\n"); + + const retirementYear = dateOfBirth.year + 65; print("\n"); - print(tab(16) + "*** YOU MAY RETIRE IN " + e + " ***\n"); + print(tab(16) + "*** YOU MAY RETIRE IN " + retirementYear + " ***\n"); print("\n"); } } diff --git a/95_Weekday/perl/README.md b/95_Weekday/perl/README.md index e69c8b81..ac8ee399 100644 --- a/95_Weekday/perl/README.md +++ b/95_Weekday/perl/README.md @@ -1,3 +1,20 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +I have replaced the manual date logic with Perl built-ins to the extent +possible. Unfortunately the kind of date math involved in the "time +spent doing ..." functionality is not well-defined, so I have been +forced to retain the original logic here. Sigh. + +You can use any punctuation character you please in the date +input. So something like 2/29/2020 is perfectly acceptable. + +It would also have been nice to produce a localized version that +supports day/month/year or year-month-day input, but that didn't happen. + +Also nice would have been language-specific output -- especially if it +could have accommodated regional differences in which day of the week or +month is unlucky. + +Tom Wyant diff --git a/95_Weekday/perl/weekday.pl b/95_Weekday/perl/weekday.pl new file mode 100755 index 00000000..e1820af6 --- /dev/null +++ b/95_Weekday/perl/weekday.pl @@ -0,0 +1,249 @@ +#!/usr/bin/env perl + +use 5.010; # To get 'state' and 'say' + +use strict; # Require explicit declaration of variables +use warnings; # Enable optional compiler warnings + +use English; # Use more friendly names for Perl's magic variables +use Term::ReadLine; # Prompt and return user input +use Time::Local qw{ timelocal }; # date-time to epoch +# FIXME timelocal() is too smart for its own good in the interpretation +# of years, and caused a bunch of Y2020 problems in Perl code that used +# it. I believe that this script avoids these problems (which only occur +# if the year is less than 1000), but it is probably safer in general to +# use timelocal_modern() or timelocal_posix(). These are also exported +# by Time::Local, but only by versions 1.28 and 1.30 respectively. This +# means that they only come (by default) with Perl 5.30 and 5.34 +# respectively. Now, Time::Local is a dual-life module, meaning it can +# be upgraded from the version packaged with older Perls. But I did not +# want to assume that it HAD been upgraded. Caveat coder. +use Time::Piece; # O-O epoch to date-time, plus formatting + +our $VERSION = '0.000_01'; + +print <<'EOD'; + + WEEKDAY + Creative Computing Morristown, New Jersey + + + +WEEKDAY is a computer demonstration that +gives facts about a date of interest to you. + +EOD + +my $now = localtime; +my $default_date = join ',', map { $now->$_() } qw{ mon mday year }; + +my $today = get_date( + "Enter today's date in the form month,day,year (default: $default_date): ", + "Please enter month,day,year or return for default\n", + $default_date, +); + +my $birthday = get_date( + 'Ender day of birth (or other day of interest): ', + "Please enter month,day,year\n", +); + +say ''; +printf "%d/%d/%d %s a %s\n", $birthday->mon, $birthday->mday, + $birthday->year, tense( $today, $birthday), + ( $birthday->mday == 13 && $birthday->wday == 6 ) ? + $birthday->fullday . ' the thirteenth --- Beware!' : + $birthday->fullday . '.'; + +if ( $birthday->epoch <= $today->epoch ) { + + say '*** Happy Birthday! ***' + if $birthday->mon == $today->mon && + $birthday->mday == $today->mday; + + print <<'EOD'; + Years Months Days + ----- ------ ---- +EOD + + my @delta = map { $today->$_() - $birthday->$_() } qw{ year mon mday }; + if ( $delta[2] < 0 ) { + $delta[2] += 30; + $delta[1] -= 1; + } + if ( $delta[1] < 0 ) { + $delta[1] += 12; + $delta[0] -= 1; + } + my @residue = @delta; + + my $delta_days = 365 * $delta[0] + 30 * $delta[1] + $delta[2]; + + display_ymd( 'Your age (if birthdate)', compute_ymd( $delta_days ) ); + display_ymd( 'You have slept', compute_ymd( $delta_days, 0.35, + \@residue ) ); + display_ymd( 'You have eaten', compute_ymd( $delta_days, 0.17, + \@residue ) ); + display_ymd( + $residue[0] > 9 ? 'You have worked/played' : + $residue[0] > 3 ? 'You have played/studied' : + 'You have played', + compute_ymd( $delta_days, 0.23, + \@residue ) ); + display_ymd( 'You have relaxed', \@residue ); + + say ''; + say "\t\t*** You may retire in @{[ $birthday->year + 65 ]} ***"; +} + +say ''; + +sub compute_ymd { + my ( $delta_days, $fract, $residue ) = @ARG; + my $days = defined $fract ? int ( $delta_days * $fract ) : $delta_days; + my $years = int( $days / 365 ); + $days -= $years * 365; + my $months = int( $days / 30 ); + $days -= $months * 30; + + if ( $residue ) { + $residue->[2] -= $days; + if ( $residue->[2] < 0 ) { + $residue->[2] += 30; + $residue->[1] -= 1; + } + $residue->[1] -= $months; + if ( $residue->[1] < 0 ) { + $residue->[1] += 12; + $residue->[0] -= 1; + } + $residue->[0] -= $years; + } + + return [ $years, $months, $days ]; +} + +sub display_ymd { + my ( $label, $ymd ) = @ARG; + printf "%-24s%4d%6d%8d\n", $label, @{ $ymd }; + return; +} + +sub get_date { + my ( $prompt, $warning, $default ) = @ARG; + my ( $month, $day, $year ) = split qr< [[:punct:]] >smx, get_input( + $prompt, + sub { + return 0 unless m/ \A (?: [0-9]+ [[:punct:]] ){2} ( [0-9]+ ) \z /smx; + return 1 if $1 >= 1582; + warn "Not prepared to give day of week prior to MDLXXXII.\n"; + return 0; + }, + $warning, + $default, + ); + return localtime timelocal( 0, 0, 0, $day, $month - 1, $year ); +} + +sub tense { + my ( $today, $birthday ) = @ARG; + my $cmp = $birthday->epoch <=> $today->epoch + or return 'is'; + return $cmp < 0 ? 'was' : 'will be'; +} + +# Get input from the user. The arguments are: +# * The prompt +# * A reference to validation code. This code receives the response in +# $ARG and returns true for a valid response. +# * A warning to print if the response is not valid. This must end in a +# return. +# * A default to return if the user simply presses . +# The first valid response is returned. An end-of-file terminates the +# script. +sub get_input { + my ( $prompt, $validate, $warning, $default ) = @ARG; + + # If no validator is passed, default to one that always returns + # true. + $validate ||= sub { 1 }; + + # Create the readline object. The 'state' causes the variable to be + # initialized only once, no matter how many times this subroutine is + # called. The do { ... } is a compound statement used because we + # need to tweak the created object before we store it. + state $term = do { + my $obj = Term::ReadLine->new( 'reverse' ); + $obj->ornaments( 0 ); + $obj; + }; + + while ( 1 ) { # Iterate indefinitely + + # Read the input into the topic variable, localized to prevent + # Spooky Action at a Distance. We exit on undef, which signals + # end-of-file. + exit unless defined( local $ARG = $term->readline( $prompt ) ); + + # Return the default if it exists AND we got an empty line + return $default if defined( $default ) && $ARG eq ''; + + # Return the input if it is valid. + return $ARG if $validate->(); + + # Issue the warning, and go around the merry-go-round again. + warn $warning; + } +} + +__END__ + +=head1 TITLE + +weekday - Play the game 'Weekday' from Basic Computer Games + +=head1 SYNOPSIS + + weekday.pl + +=head1 DETAILS + +This Perl script is a port of weekday.bas, which is the 95th entry in +Basic Computer Games. + +I have replaced the manual date logic with Perl built-ins to the extent +possible. Unfortunately the kind of date math involved in the "time +spent doing ..." functionality is not well-defined, so I have been +forced to retain the original logic here. Sigh. + +You can use any punctuation character you please in the date +input. So something like 2/29/2020 is perfectly acceptable. + +It would also have been nice to produce a localized version that +supports day/month/year or year-month-day input, but that didn't happen. + +Also nice would have been language-specific output -- especially if it +could have accommodated regional differences in which day of the week or +month is unlucky. + +=head1 PORTED BY + +Thomas R. Wyant, III F + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2022 by Thomas R. Wyant, III + +This program is free software; you can redistribute it and/or modify it +under the same terms as Perl 5.10.0. For more details, see the Artistic +License 1.0 at +L, and/or the +Gnu GPL at L. + +This program is distributed in the hope that it will be useful, but +without any warranty; without even the implied warranty of +merchantability or fitness for a particular purpose. + +=cut + +# ex: set expandtab tabstop=4 textwidth=72 :