Merge branch 'coding-horror:main' into main

This commit is contained in:
Roy Gilliam
2022-02-13 16:48:01 -05:00
committed by GitHub
25 changed files with 2405 additions and 193 deletions

View File

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

View File

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

92
06_Banner/ruby/banner.rb Executable file
View File

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

View File

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

View File

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

View File

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

56
14_Bowling/csharp/Pins.cs Normal file
View File

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

View File

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

View File

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

View File

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

87
19_Bunny/csharp/Bunny.cs Normal file
View File

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

View File

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

45
52_Kinema/ruby/kinema.rb Normal file
View File

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

45
54_Letter/ruby/letter.rb Normal file
View File

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

43
58_Love/ruby/love.rb Normal file
View File

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

View File

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

419
60_Mastermind/perl/mastermind.pl Executable file
View File

@@ -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 <return> 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<wyant at cpan dot org>
=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<https://www.perlfoundation.org/artistic-license-10.html>, and/or the
Gnu GPL at L<http://www.gnu.org/licenses/old-licenses/gpl-1.0.txt>.
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 :

View File

@@ -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 = <STDIN>);
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;

View File

@@ -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<Stock> 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<Stock> 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<Stock> 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<Stock> 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<Stock> initStocks() {
List<Stock> 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 +
'}';
}
}
}

View File

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

View File

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

118
94_War/ruby/war.rb Normal file
View File

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

View File

@@ -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<String>}
*/
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<DateStruct>}
*/
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");
}
}

View File

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

249
95_Weekday/perl/weekday.pl Executable file
View File

@@ -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 <return>.
# 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<wyant at cpan dot org>
=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<https://www.perlfoundation.org/artistic-license-10.html>, and/or the
Gnu GPL at L<http://www.gnu.org/licenses/old-licenses/gpl-1.0.txt>.
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 :