diff --git a/00_Alternate_Languages/78_Sine_Wave/C++/README.md b/00_Alternate_Languages/78_Sine_Wave/C++/README.md new file mode 100644 index 00000000..b62763ff --- /dev/null +++ b/00_Alternate_Languages/78_Sine_Wave/C++/README.md @@ -0,0 +1,3 @@ +Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) + +Conversion to [C++17](https://en.wikipedia.org/wiki/C%2B%2B17) \ No newline at end of file diff --git a/00_Alternate_Languages/78_Sine_Wave/C++/sinewave.cpp b/00_Alternate_Languages/78_Sine_Wave/C++/sinewave.cpp new file mode 100644 index 00000000..9d274e56 --- /dev/null +++ b/00_Alternate_Languages/78_Sine_Wave/C++/sinewave.cpp @@ -0,0 +1,21 @@ +#include // std::cout, std::endl +#include // std::string(size_t n, char c) +#include // std::sin(double x) + +int main() +{ + std::cout << std::string(30, ' ') << "SINE WAVE" << std::endl; + std::cout << std::string(15, ' ') << "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY" << std::endl; + std::cout << std::string(5, '\n'); + + bool b = true; + + for (double t = 0.0; t <= 40.0; t += 0.25) + { + int a = int(26 + 25 * std::sin(t)); + std::cout << std::string(a, ' ') << (b ? "CREATIVE" : "COMPUTING") << std::endl; + b = !b; + } + + return 0; +} diff --git a/03_Animal/lua/Animal.lua b/03_Animal/lua/Animal.lua new file mode 100644 index 00000000..293c53e4 --- /dev/null +++ b/03_Animal/lua/Animal.lua @@ -0,0 +1,181 @@ +-- +-- Animal.lua +-- + +-- maintain a flat table/list of all the known animals +local animals = { + "FISH", + "BIRD" +} + +-- store the questions as a binary tree with each node having a +-- "y" or "n" branch +-- if the node has a member named "a" it's an answer, with an index +-- into the animals list. +-- Otherwise, it's a question that leads to more nodes. +local questionTree = { + q = "DOES IT SWIM", + y = { a = 1 }, + n = { a = 2 } +} + +-- print the given prompt string and then wait for input +-- loops until a non-empty input is given +-- returns the input as an upper-case string +function askPrompt(promptString) + local answered = false + local a + while (not answered) do + print(promptString) + a = io.read() + a = string.upper(a) + if (string.len(a) > 0) then + answered = true + end + end + return a +end + +-- print the given prompt string and then wait for the +-- user to enter a string beginning with "Y" or "N" +function askYesOrNo(promptString) + local a + while ((a ~= "Y") and (a ~= "N")) do + a = askPrompt(promptString) + a = a:sub(1,1) + end + return a +end + +-- prints the introductory text from the original BASIC program +function printIntro() + print(string.format("%32s", " ") .. "ANIMAL") + print(string.format("%15s", " ") .. "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY") + print() + print() + print() + print("PLAY 'GUESS THE ANIMAL'") + print("THINK OF AN ANIMAL AND THE COMPUTER WILL TRY TO GUESS IT.") + print() +end + +-- prints the animals known in the source list +function listKnownAnimals() + print() + print("ANIMALS I ALREADY KNOW ARE:") + + local x + local item + for x = 1,#animals do + -- use string.format to space each animal in a 12-character-wide "cell" + item = string.format("%-12s", animals[x]) + + -- io.write() works like print(), but doesn't automatically add a carriage return/newline + io.write(item) + + -- every fifth item, start a new line + if ((x % 5) == 0) then + io.write("\n") + end + end + + print() + print() +end + +-- Prompts the user for info about the animal they were thinking of, then +-- uses that to add a new branch to the tree +-- curNode: the node in the tree where the computer made a wrong guess +-- branch: the answer the user gave to curNode's question +function addAnimalToTree(curNode, branch) + local newAnimal + local curResponse = curNode[branch] + local guessedIndex = curResponse.a + local guessedAnimal = animals[guessedIndex] + local newQuestion, newAnswer, newIndex + local newNode + + newAnimal = askPrompt("THE ANIMAL YOU WERE THINKING OF WAS A ?") + newQuestion = askPrompt("PLEASE TYPE IN A QUESTION THAT WOULD DISTINGUISH A ".. + tostring(newAnimal).." FROM A "..tostring(guessedAnimal)) + newAnswer = askYesOrNo("FOR A "..tostring(newAnimal).." THE ANSWER WOULD BE?") + + -- add the new animal to the master list at the end, and + -- save off its index in the list + table.insert(animals, newAnimal) + newIndex = #animals + + -- create a new node for the question we just learned + newNode = {} + newNode.q = newQuestion + if (newAnswer == "Y") then + newNode.y = { a = newIndex } + newNode.n = { a = guessedIndex } + else + newNode.y = { a = guessedIndex } + newNode.n = { a = newIndex } + end + + -- replace the previous answer with our new node + curNode[branch] = newNode +end + +-- Starts at the root of the question tree and asks questions about +-- the user's animal until the computer hits an "a" answer node and tries +-- to make a guess +function askAboutAnimal() + local curNode = questionTree + local finished = false + local response, responseIndex + local nextNode, animalName + while (not finished) do + response = askYesOrNo(curNode.q .. "?") + + -- convert the response "Y" or "N" to the lowercase "y" or "n" that we use to name our branches + branch = string.lower(response) + nextNode = curNode[branch] + + -- is the next node an answer node, or another question? + if (nextNode.a ~= nil) then + -- it's an answer, so make a guess + animalName = animals[nextNode.a] + response = askYesOrNo("IS IT A "..tostring(animalName).."?") + if (response == "Y") then + -- we got the correct answer, so prompt for a new animal + print() + print("WHY NOT TRY ANOTHER ANIMAL?") + else + -- incorrect answer, so add a new entry at this point in the tree + addAnimalToTree(curNode, branch) + end + + -- whether we were right or wrong, we're finished with this round + finished = true + else + -- it's another question, so advance down the tree + curNode = nextNode + end + end +end + +-- MAIN CONTROL SECTION + +printIntro() + +-- loop forever until the player requests an exit by entering a blank line +local exitRequested = false +local answer + +while (not exitRequested) do + print("ARE YOU THINKING OF AN ANIMAL?") + answer = io.read() + answer = string.upper(answer) + + if (string.len(answer) == 0) then + exitRequested = true + elseif (answer:sub(1,4) == "LIST") then + listKnownAnimals() + elseif (answer:sub(1,1) == "Y") then + askAboutAnimal() + end +end diff --git a/06_Banner/perl/banner.pl b/06_Banner/perl/banner.pl new file mode 100644 index 00000000..2524ef00 --- /dev/null +++ b/06_Banner/perl/banner.pl @@ -0,0 +1,150 @@ +#!/usr/bin/perl + +# Banner program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +sub print_lines +{ + my $lines = shift; + print "\n" x $lines; +} + +# each letter is made of 7 slices (or rows); +# the initial & unused 0 is to allow for Perl arrays being 0-based +# but allow the algorithm to be 1-based (like Basic arrays); +# the numbers are essentially the dots/columns per slice in powers of 2. +my %data = ( + " " => [ 0, 0, 0, 0, 0, 0, 0, 0 ], + "." => [ 0, 1, 1, 129, 449, 129, 1, 1 ], + "!" => [ 0, 1, 1, 1, 384, 1, 1, 1 ], + "=" => [ 0, 41, 41, 41, 41, 41, 41, 41 ], + "?" => [ 0, 5, 3, 2, 354, 18, 11, 5 ], + "*" => [ 0, 69, 41, 17, 512, 17, 41, 69 ], + "0" => [ 0, 57, 69, 131, 258, 131, 69, 57 ], + "1" => [ 0, 0, 0, 261, 259, 512, 257, 257 ], + "2" => [ 0, 261, 387, 322, 290, 274, 267, 261 ], + "3" => [ 0, 66, 130, 258, 274, 266, 150, 100 ], + "4" => [ 0, 33, 49, 41, 37, 35, 512, 33 ], + "5" => [ 0, 160, 274, 274, 274, 274, 274, 226 ], + "6" => [ 0, 194, 291, 293, 297, 305, 289, 193 ], + "7" => [ 0, 258, 130, 66, 34, 18, 10, 8 ], + "8" => [ 0, 69, 171, 274, 274, 274, 171, 69 ], + "9" => [ 0, 263, 138, 74, 42, 26, 10, 7 ], + "A" => [ 0, 505, 37, 35, 34, 35, 37, 505 ], + "B" => [ 0, 512, 274, 274, 274, 274, 274, 239 ], + "C" => [ 0, 125, 131, 258, 258, 258, 131, 69 ], + "D" => [ 0, 512, 258, 258, 258, 258, 131, 125 ], + "E" => [ 0, 512, 274, 274, 274, 274, 258, 258 ], + "F" => [ 0, 512, 18, 18, 18, 18, 2, 2 ], + "G" => [ 0, 125, 131, 258, 258, 290, 163, 101 ], + "H" => [ 0, 512, 17, 17, 17, 17, 17, 512 ], + "I" => [ 0, 258, 258, 258, 512, 258, 258, 258 ], + "J" => [ 0, 65, 129, 257, 257, 257, 129, 128 ], + "K" => [ 0, 512, 17, 17, 41, 69, 131, 258 ], + "L" => [ 0, 512, 257, 257, 257, 257, 257, 257 ], + "M" => [ 0, 512, 7, 13, 25, 13, 7, 512 ], + "N" => [ 0, 512, 7, 9, 17, 33, 193, 512 ], + "O" => [ 0, 125, 131, 258, 258, 258, 131, 125 ], + "P" => [ 0, 512, 18, 18, 18, 18, 18, 15 ], + "Q" => [ 0, 125, 131, 258, 258, 322, 131, 381 ], + "R" => [ 0, 512, 18, 18, 50, 82, 146, 271 ], + "S" => [ 0, 69, 139, 274, 274, 274, 163, 69 ], + "T" => [ 0, 2, 2, 2, 512, 2, 2, 2 ], + "U" => [ 0, 128, 129, 257, 257, 257, 129, 128 ], + "V" => [ 0, 64, 65, 129, 257, 129, 65, 64 ], + "W" => [ 0, 256, 257, 129, 65, 129, 257, 256 ], + "X" => [ 0, 388, 69, 41, 17, 41, 69, 388 ], + "Y" => [ 0, 8, 9, 17, 481, 17, 9, 8 ], + "Z" => [ 0, 386, 322, 290, 274, 266, 262, 260 ], +); + +my ($horz, $vert, $center, $char, $msg) = (0, 0, '', '', ''); + +# get args to run with +while ($horz < 1) +{ + print "HORIZONTAL (1 or more): "; + chomp($horz = <>); + $horz = int($horz); +} + +while ($vert < 1) +{ + print "VERTICAL (1 or more): "; + chomp($vert = <>); + $vert = int($vert); +} + +print "CENTERED (Y/N): "; +chomp($center = <>); +$center = ($center =~ m/^Y/i) ? 1 : 0; + +# note you can enter multiple chars and the program will do the right thing +# thanks to the length() calls below, which was in the original Basic +print "CHARACTER TO PRINT (TYPE 'ALL' IF YOU WANT CHARACTER BEING PRINTED): "; +chomp($char = uc(<>)); + +while (!$msg) +{ + print "STATEMENT: "; + chomp($msg = uc(<>)); +} + +print "SET PAGE TO PRINT, HIT RETURN WHEN READY"; +$_ = <>; +print_lines(2 * $horz); + +# print the message +for my $letter ( split(//, $msg) ) +{ + if (!exists($data{$letter})) + { + die "Cannot use letter '$letter'!"; + } + my @s = @{$data{$letter}}; + #if ($letter eq " ") { print_lines(7 * $horz); next; } + + my $print_letter = ($char eq "ALL") ? $letter : $char; + for my $slice (1 .. 7) + { + my (@j, @f); + for (my $k = 8; $k >= 0; $k--) + { + if (2**$k < $s[$slice]) + { + $j[9 - $k] = 1; + $s[$slice] = $s[$slice] - 2**$k; + if ($s[$slice] == 1) + { + $f[$slice] = 9 - $k; + } + } + else + { + $j[9 - $k] = 0; + } + } + + for my $t1 (1 .. $horz) + { + print " " x int((63 - 4.5 * $vert) * $center / (length($print_letter)) + 1); + for my $b (1 .. (defined($f[$slice]) ? $f[$slice] : 0)) + { + my $str = $j[$b] ? $print_letter : (" " x length($print_letter)); + print $str for (1 .. $vert); + } + print "\n"; + } + } + + # space between letters + print_lines(2 * $horz); +} + +# while in the original code, this seems pretty excessive +#print_lines(75); + +exit(0); diff --git a/07_Basketball/perl/README.md b/07_Basketball/perl/README.md index e69c8b81..51373c0c 100644 --- a/07_Basketball/perl/README.md +++ b/07_Basketball/perl/README.md @@ -1,3 +1,21 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +There are two version of the code here, a "faithful" translation (basketball-orig.pl) and +a "modern" translation (basketball.pl). The main difference between the 2 are is that the +faithful translation has 3 GOTOs in it while the modern version has no GOTO. I have added +a "TIME" print when the score is shown so the Clock is visible. Halftime is at "50" and +end of game is at 100 (per the Basic code). + +The 3 GOTOs in the faitful version are because of the way the original code jumped into +the "middle of logic" that has no obivious way to avoid ... that I can see, at least while +still maintaining something of the look and structure of the original Basic. + +The modern version avoided the GOTOs by restructuring the program in the 2 "play()" subs. +Despite the change, this should play the same way as the faithful version. + +All of the percentages remain the same. If writing this from scratch, we really should +have only a single play() sub which uses the same code for both teams, which would also +make the game more fair ... but that wasn't done so the percent edge to Darmouth has been +maintained here. diff --git a/07_Basketball/perl/basketball-orig.pl b/07_Basketball/perl/basketball-orig.pl new file mode 100755 index 00000000..14627ffc --- /dev/null +++ b/07_Basketball/perl/basketball-orig.pl @@ -0,0 +1,415 @@ +#!/usr/bin/perl + +# Basketball program in Perl +# This is fairly faithful translation from the original Basic. +# This becomes apparent because there are actually 3 GOTOs still present +# because of the way the original code jumped into the "middle of logic" +# that has no obivious way to avoid ... that I can see. +# For better structure and no GOTOs, see the other version of this program. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $Defense=0; # dartmouth defense value +my $Opponent; # name of opponent +my @Score = (0, 0); # scores, dart is [0], opponent is [1] +my $Player = 0; # player, 0 = dart, 1 = opp +my $Timer = 0; # time tick, 100 ticks per game, 50 is end of first half, if tie at end then back to T=93 +my $DoPlay = 1; # true if game is still being played +my $ConTeam; # controlling team, "dart" or "opp" +my $ShotType = 0; # current shot type + + +print "\n"; +print " " x 31, "BASKETBALL"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"; +print "\n\n\n"; + +print "THIS IS DARTMOUTH COLLEGE BASKETBALL. YOU WILL BE DARTMOUTH\n"; +print "CAPTAIN AND PLAYMAKER. CALL SHOTS AS FOLLOWS:\n"; +print " 1. LONG (30 FT.) JUMP SHOT;\n"; +print " 2. SHORT (15 FT.) JUMP SHOT;\n"; +print " 3. LAY UP;\n"; +print " 4. SET SHOT.\n"; +print "BOTH TEAMS WILL USE THE SAME DEFENSE. CALL DEFENSE AS FOLLOWS:\n"; +print " 6. PRESS;\n"; +print " 6.5 MAN-TO MAN;\n"; +print " 7. ZONE;\n"; +print " 7.5 NONE.\n"; +print "TO CHANGE DEFENSE, JUST TYPE 0 AS YOUR NEXT SHOT.\n\n"; +get_defense(); +print "\n"; +print "CHOOSE YOUR OPPONENT: "; +chomp($Opponent = <>); + +$ConTeam = center_jump(); +while ($DoPlay) +{ + print "\n"; + if ($ConTeam eq "dart") + { + $Player = 0; + get_your_shot(); + dartmouth_play(); + } + else + { + opponent_play(); + } + if ($Timer >= 100) + { + check_end_game(); + last if (!$DoPlay); + $Timer = 93; + $ConTeam = center_jump(); + } +} +exit(0); + +############################################################### + +sub dartmouth_play +{ + if ($ShotType == 1 || $ShotType == 2) + { + $Timer++; + if ($Timer == 50) + { + end_first_half(); + return; + } + if ($Timer == 92) + { + two_min_left(); + } + + print "JUMP SHOT\n"; + if (rand(1) <= 0.341 * $Defense / 8) + { + print "SHOT IS GOOD.\n"; + dartmouth_score(); + $ConTeam = "opp"; + return; + } + + if (rand(1) <= 0.682*$Defense/8) + { + print "SHOT IS OFF TARGET.\n"; + if ($Defense/6*rand(1) > 0.45) + { + print "REBOUND TO ", $Opponent, "\n"; + $ConTeam = "opp"; + return; + } + + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + if (rand(1) <= 0.4) { goto L1300; } + if ($Defense == 6) + { + if (rand(1) <= 0.6) + { + print "PASS STOLEN BY $Opponent, EASY LAYUP.\n"; + opp_score(); + $ConTeam = "dart"; return; + return; + } + } + print "BALL PASSED BACK TO YOU.\n"; + $ConTeam = "dart"; + return; + } + + if (rand(1) <= 0.782*$Defense/8) + { + print "SHOT IS BLOCKED. BALL CONTROLLED BY "; # no NL + if (rand(1) > 0.5) + { + print "$Opponent.\n"; + $ConTeam = "opp"; + return; + } + else + { + print "DARTMOUTH.\n"; + $ConTeam = "dart"; + return; + } + } + + if (rand(1) > 0.843*$Defense/8) + { + print "CHARGING FOUL. DARTMOUTH LOSES BALL.\n"; + $ConTeam = "opp"; + return; + } + else + { + print "SHOOTER IS FOULED. TWO SHOTS.\n"; + foul_shooting(); + $ConTeam = "opp"; + return; + } + } + + L1300: + while (1) + { + $Timer++; + if ($Timer == 50) + { + end_first_half(); + return; + } + if ($Timer == 92) { two_min_left(); } + if ($ShotType == 0) + { + get_defense(); + return; + } + print '', ($ShotType > 3 ? "SET SHOT." : "LAY UP."), "\n"; + if (7 / $Defense * rand(1) <= 0.4) + { + print "SHOT IS GOOD. TWO POINTS.\n"; + dartmouth_score(); + $ConTeam = "opp"; + return; + } + + if (7 / $Defense * rand(1) <= 0.7) + { + print "SHOT IS OFF THE RIM.\n"; + if (rand(1) <= 0.667) + { + print "$Opponent CONTROLS THE REBOUND.\n"; + $ConTeam = "opp"; + return; + } + + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + next if (rand(1) <= 0.4); + + print "BALL PASSED BACK TO YOU.\n"; + $ConTeam = "dart"; + return; + } + + if (7 / $Defense * rand(1) <= 0.875) + { + print "SHOOTER FOULED. TWO SHOTS.\n"; + foul_shooting(); + $ConTeam = "opp"; + return; + } + + if (7 / $Defense * rand(1) <= 0.925) + { + print "SHOT BLOCKED. $Opponent\'S BALL.\n"; + $ConTeam = "opp"; + return; + } + + print "CHARGING FOUL. DARTMOUTH LOSES THE BALL.\n"; + $ConTeam = "opp"; + return; + } +} + +sub get_defense +{ + $Defense = 0; + while ($Defense < 6 || $Defense > 7.5) + { + print "YOUR NEW DEFENSIVE ALLIGNMENT IS (6, 6.5, 7. 7.5): "; + chomp($Defense = <>); + ($Defense) =~ m/(\d(\.\d)?)/; + } +} + +sub opponent_play +{ + $Player = 1; + $Timer++; + if ($Timer == 50) + { + end_first_half(); + $ConTeam = center_jump(); + return; + } + + print "\n"; + while (1) + { + my $shot = 10.0 / 4 * rand(1) + 1; + if ($shot <= 2.0) + { + print "JUMP SHOT.\n"; + if (8.0 / $Defense * rand(1) <= 0.35) + { + print "SHOT IS GOOD.\n"; + opp_score(); + $ConTeam = "dart"; + return; + } + + if (8.0 / $Defense * rand(1) <= 0.75) + { + print "SHOT IS OFF RIM.\n"; + + L3110: + if ($Defense / 6.0 * rand(1) <= 0.5) + { + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + $ConTeam = "dart"; + return; + } + print "$Opponent CONTROLS THE REBOUND.\n"; + if ($Defense == 6) + { + if (rand(1) <= 0.75) + { + print "BALL STOLEN. EASY LAY UP FOR DARTMOUTH.\n"; + dartmouth_score(); + $ConTeam = "opp"; + return; + } + } + if (rand(1) <= 0.5) + { + print "PASS BACK TO $Opponent GUARD.\n"; + $ConTeam = "opp"; + return; + } + goto L3500; + } + + if (8.0 / $Defense * rand(1) <= 0.9) + { + print "PLAYER FOULED. TWO SHOTS.\n"; + foul_shooting(); + $ConTeam = "dart"; + return; + } + print "OFFENSIVE FOUL. DARTMOUTH'S BALL.\n"; + $ConTeam = "dart"; + return; + } + + L3500: + print ($shot > 3 ? "SET SHOT.\n" : "LAY UP.\n"); + if (7.0 / $Defense * rand(1) > 0.413) + { + print "SHOT IS MISSED.\n"; + { + no warnings; + goto L3110; + } + } + else + { + print "SHOT IS GOOD.\n"; + opp_score(); + $ConTeam = "dart"; + return; + } + } +} + +sub opp_score +{ + $Score[0] += 2; + print_score(); +} + +sub dartmouth_score +{ + $Score[1] += 2; + print_score(); +} + +sub print_score +{ + print "SCORE: $Score[1] TO $Score[0]\n"; + print "TIME: $Timer\n"; +} + +sub end_first_half +{ + print "\n ***** END OF FIRST HALF *****\n\n"; + print "SCORE: DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n\n"; + center_jump(); +} + +sub get_your_shot +{ + $ShotType = -1; + while ($ShotType < 0 || $ShotType > 4) + { + print "YOUR SHOT (0-4): "; + chomp($ShotType = <>); + $ShotType = int($ShotType); + if ($ShotType < 0 || $ShotType > 4) + { + print "INCORRECT ANSWER. RETYPE IT. "; + } + } +} + +sub center_jump +{ + print "CENTER JUMP\n"; + if (rand(1) <= 0.6) + { + print "$Opponent CONTROLS THE TAP.\n"; + return "opp"; + } + print "DARTMOUTH CONTROLS THE TAP.\n"; + return "dart"; +} + +sub check_end_game +{ + print "\n"; + if ($Score[1] != $Score[0]) + { + print " ***** END OF GAME *****\n"; + print "FINAL SCORE: DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n"; + $DoPlay = 0; + } + else + { + print "\n ***** END OF SECOND HALF *****\n"; + print "SCORE AT END OF REGULATION TIME:\n"; + print " DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n"; + print "BEGIN TWO MINUTE OVERTIME PERIOD\n"; + } +} + +sub two_min_left +{ + print "\n *** TWO MINUTES LEFT IN THE GAME ***\n\n"; +} + +sub foul_shooting +{ + if (rand(1) > 0.49) + { + if (rand(1) > 0.75) + { + print "BOTH SHOTS MISSED.\n"; + } + else + { + print "SHOOTER MAKES ONE SHOT AND MISSES ONE.\n"; + $Score[1 - $Player]++; + } + } + else + { + print "SHOOTER MAKES BOTH SHOTS.\n"; + $Score[1 - $Player] += 2; + } + + print_score(); +} diff --git a/07_Basketball/perl/basketball.pl b/07_Basketball/perl/basketball.pl new file mode 100755 index 00000000..ac348309 --- /dev/null +++ b/07_Basketball/perl/basketball.pl @@ -0,0 +1,393 @@ +#!/usr/bin/perl + +# Basketball program in Perl +# While this should play the same way as the fairly faithful translation version, +# there are no GOTOs in this code. That was achieved by restructuring the program +# in the 2 *_play() subs. All of the percentages remain the same. If writing this +# from scratch, we really should have only a play() sub which uses the same code +# for both teams, but the percent edge to Darmouth has been maintained here. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $Defense=0; # dartmouth defense value +my $Opponent; # name of opponent +my @Score = (0, 0); # scores, dart is [0], opponent is [1] +my $Player = 0; # player, 0 = dart, 1 = opp +my $Timer = 0; # time tick, 100 ticks per game, 50 is end of first half, if tie at end then back to T=93 +my $DoPlay = 1; # true if game is still being played +my $ConTeam; # controlling team, "dart" or "opp" +my $ShotType = 0; # current shot type + + +print "\n"; +print " " x 31, "BASKETBALL"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"; +print "\n\n\n"; + +print "THIS IS DARTMOUTH COLLEGE BASKETBALL. YOU WILL BE DARTMOUTH\n"; +print "CAPTAIN AND PLAYMAKER. CALL SHOTS AS FOLLOWS:\n"; +print " 1. LONG (30 FT.) JUMP SHOT;\n"; +print " 2. SHORT (15 FT.) JUMP SHOT;\n"; +print " 3. LAY UP;\n"; +print " 4. SET SHOT.\n"; +print "BOTH TEAMS WILL USE THE SAME DEFENSE. CALL DEFENSE AS FOLLOWS:\n"; +print " 6. PRESS;\n"; +print " 6.5 MAN-TO MAN;\n"; +print " 7. ZONE;\n"; +print " 7.5 NONE.\n"; +print "TO CHANGE DEFENSE, JUST TYPE 0 AS YOUR NEXT SHOT.\n\n"; +get_defense(); +print "\n"; +print "CHOOSE YOUR OPPONENT: "; +chomp($Opponent = <>); + +$ConTeam = center_jump(); +while ($DoPlay) +{ + print "\n"; + if ($ConTeam eq "dart") + { + get_your_shot(); + dartmouth_play(); + } + else + { + $ShotType = 10.0 / 4 * rand(1) + 1; + opponent_play(); + } + if ($Timer >= 100) + { + check_end_game(); + last if (!$DoPlay); + $Timer = 93; + $ConTeam = center_jump(); + } +} +exit(0); + +############################################################### + +sub dartmouth_play +{ + $Player = 0; + print "\n"; + while (1) + { + $Timer++; + if ($Timer == 50) { end_first_half(); return; } + if ($Timer == 92) { two_min_left(); } + + if ($ShotType == 0) + { + get_defense(); + return; # for new ShotType + } + elsif ($ShotType == 1 || $ShotType == 2) + { + print "JUMP SHOT\n"; + if (rand(1) <= 0.341 * $Defense / 8) + { + print "SHOT IS GOOD.\n"; + dartmouth_score(); + last; + } + + if (rand(1) <= 0.682*$Defense/8) + { + print "SHOT IS OFF TARGET.\n"; + if ($Defense/6*rand(1) > 0.45) + { + print "REBOUND TO $Opponent\n"; + last; + } + + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + if (rand(1) <= 0.4) + { + $ShotType = (rand(1) <= 0.5) ? 3 : 4; + next; + } + if ($Defense == 6) + { + if (rand(1) <= 0.6) + { + print "PASS STOLEN BY $Opponent, EASY LAYUP.\n"; + opp_score(); + next; + } + } + print "BALL PASSED BACK TO YOU.\n"; + next; + } + + if (rand(1) <= 0.782*$Defense/8) + { + print "SHOT IS BLOCKED. BALL CONTROLLED BY "; # no NL + if (rand(1) > 0.5) + { + print "$Opponent.\n"; + last; + } + else + { + print "DARTMOUTH.\n"; + next; + } + } + + if (rand(1) > 0.843*$Defense/8) + { + print "CHARGING FOUL. DARTMOUTH LOSES BALL.\n"; + last; + } + else + { + print "SHOOTER IS FOULED. TWO SHOTS.\n"; + foul_shooting(); + last; + } + } + else # elsif ($ShotType >= 3) + { + print '', ($ShotType > 3 ? "SET SHOT." : "LAY UP."), "\n"; + if (7 / $Defense * rand(1) <= 0.4) + { + print "SHOT IS GOOD. TWO POINTS.\n"; + dartmouth_score(); + last; + } + + if (7 / $Defense * rand(1) <= 0.7) + { + print "SHOT IS OFF THE RIM.\n"; + if (rand(1) <= 0.667) + { + print "$Opponent CONTROLS THE REBOUND.\n"; + last; + } + + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + next if (rand(1) <= 0.4); + + print "BALL PASSED BACK TO YOU.\n"; + next; + } + + if (7 / $Defense * rand(1) <= 0.875) + { + print "SHOOTER FOULED. TWO SHOTS.\n"; + foul_shooting(); + last; + } + + if (7 / $Defense * rand(1) <= 0.925) + { + print "SHOT BLOCKED. $Opponent\'S BALL.\n"; + last; + } + + print "CHARGING FOUL. DARTMOUTH LOSES THE BALL.\n"; + last; + } + } + $ConTeam = "opp"; +} + +sub get_defense +{ + $Defense = 0; + do { + print "YOUR NEW DEFENSIVE ALLIGNMENT IS (6, 6.5, 7. 7.5): "; + chomp($Defense = <>); + ($Defense) =~ m/(\d(\.\d)?)/; + } while ($Defense < 6.0 || $Defense > 7.5) +} + +sub opponent_play +{ + $Player = 1; + print "\n"; + while (1) + { + $Timer++; + if ($Timer == 50) { end_first_half(); return; } + if ($Timer == 92) { two_min_left(); } + + if ($ShotType <= 2.0) + { + print "JUMP SHOT.\n"; + if (8.0 / $Defense * rand(1) <= 0.35) + { + print "SHOT IS GOOD.\n"; + opp_score(); + last; + } + + if (8.0 / $Defense * rand(1) <= 0.75) + { + print "SHOT IS OFF RIM.\n"; + opp_missed(); + return; # for possible new ShotType or team change + } + + if (8.0 / $Defense * rand(1) <= 0.9) + { + print "PLAYER FOULED. TWO SHOTS.\n"; + foul_shooting(); + last; + } + print "OFFENSIVE FOUL. DARTMOUTH'S BALL.\n"; + last; + } + else # ShotType >= 3 + { + print ($ShotType > 3 ? "SET SHOT.\n" : "LAY UP.\n"); + if (7.0 / $Defense * rand(1) > 0.413) + { + print "SHOT IS MISSED.\n"; + { + opp_missed(); + return; # for possible new ShotType or team change + } + } + else + { + print "SHOT IS GOOD.\n"; + opp_score(); + last; + } + } + } + $ConTeam = "dart"; +} + +sub opp_missed +{ + if ($Defense / 6.0 * rand(1) <= 0.5) + { + print "DARTMOUTH CONTROLS THE REBOUND.\n"; + $ConTeam = "dart"; + } + else + { + print "$Opponent CONTROLS THE REBOUND.\n"; + if ($Defense == 6) + { + if (rand(1) <= 0.75) + { + print "BALL STOLEN. EASY LAY UP FOR DARTMOUTH.\n"; + dartmouth_score(); + #$ConTeam = "opp"; + return; # for possible new ShotType + } + } + if (rand(1) <= 0.5) + { + print "PASS BACK TO $Opponent GUARD.\n"; + #$ConTeam = "opp"; + return; # for possible new ShotType + } + $ShotType = (rand(1) <= 0.5) ? 3 : 4; + } +} + +sub opp_score +{ + $Score[0] += 2; + print_score(); +} + +sub dartmouth_score +{ + $Score[1] += 2; + print_score(); +} + +sub print_score +{ + print "SCORE: $Score[1] TO $Score[0]\n"; + print "TIME: $Timer\n"; +} + +sub end_first_half +{ + print "\n ***** END OF FIRST HALF *****\n\n"; + print "SCORE: DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n\n"; + $ConTeam = center_jump(); +} + +sub get_your_shot +{ + $ShotType = -1; + while ($ShotType < 0 || $ShotType > 4) + { + print "YOUR SHOT (0-4): "; + chomp($ShotType = <>); + $ShotType = int($ShotType); + if ($ShotType < 0 || $ShotType > 4) + { + print "INCORRECT ANSWER. RETYPE IT. "; + } + } +} + +sub center_jump +{ + print "CENTER JUMP\n"; + if (rand(1) <= 0.6) + { + print "$Opponent CONTROLS THE TAP.\n"; + return "opp"; + } + print "DARTMOUTH CONTROLS THE TAP.\n"; + return "dart"; +} + +sub check_end_game +{ + print "\n"; + if ($Score[1] != $Score[0]) + { + print " ***** END OF GAME *****\n"; + print "FINAL SCORE: DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n"; + $DoPlay = 0; + } + else + { + print "\n ***** END OF SECOND HALF *****\n"; + print "SCORE AT END OF REGULATION TIME:\n"; + print " DARTMOUTH: $Score[1] $Opponent: $Score[0]\n\n"; + print "BEGIN TWO MINUTE OVERTIME PERIOD\n"; + } +} + +sub two_min_left +{ + print "\n *** TWO MINUTES LEFT IN THE GAME ***\n\n"; +} + +sub foul_shooting +{ + if (rand(1) > 0.49) + { + if (rand(1) > 0.75) + { + print "BOTH SHOTS MISSED.\n"; + } + else + { + print "SHOOTER MAKES ONE SHOT AND MISSES ONE.\n"; + $Score[1 - $Player]++; + } + } + else + { + print "SHOOTER MAKES BOTH SHOTS.\n"; + $Score[1 - $Player] += 2; + } + + print_score(); +} diff --git a/13_Bounce/perl/README.md b/13_Bounce/perl/README.md index e69c8b81..6d213dba 100644 --- a/13_Bounce/perl/README.md +++ b/13_Bounce/perl/README.md @@ -1,3 +1,6 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +Added feature so that if "TIME" value is "0" then it will quit, +so you don't have to hit Control-C. Also added a little error checking of the input. diff --git a/13_Bounce/perl/bounce.pl b/13_Bounce/perl/bounce.pl new file mode 100755 index 00000000..4aeacf1f --- /dev/null +++ b/13_Bounce/perl/bounce.pl @@ -0,0 +1,97 @@ +#!/usr/bin/perl + +# Bounce program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +print "\n"; +print " " x 31,"BOUNCE\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n"; +print "\n\n\n"; + +print "THIS SIMULATION LETS YOU SPECIFY THE INITIAL VELOCITY\n"; +print "OF A BALL THROWN STRAIGHT UP, AND THE COEFFICIENT OF\n"; +print "ELASTICITY OF THE BALL. PLEASE USE A DECIMAL FRACTION\n"; +print "COEFFICIENCY (LESS THAN 1).\n\n"; +print "YOU ALSO SPECIFY THE TIME INCREMENT TO BE USED IN\n"; +print "'STROBING' THE BALL'S FLIGHT (TRY .1 INITIALLY).\n\n"; + +# deal with basic's tab() for line positioning +# line = line string we're starting with +# pos = position to start writing +# s = string to write +# returns the resultant string, which might not have been changed +sub line_tab +{ + my ($line, $pos, $s) = @_; + my $len = length($line); + # if curser is past position, do nothing + if ($len <= $pos) { $line .= " " x ($pos - $len) . $s; } + return $line; +} + +while (1) +{ + my @T; # time slice? + my $time_inc; # time increment, probably in fractions of seconds + my $velocity; # velocity in feet/sec + my $coeff_elas; # coeeficent of elasticity + my $line_pos; # position on line + my $S1 # duration in full seconds? + + # get input + print "TIME INCREMENT (SEC, 0=QUIT): "; chomp($time_inc = <>); + last if ($time_inc == 0); + print "VELOCITY (FPS): "; chomp($velocity = <>); + print "COEFFICIENT: "; chomp($coeff_elas = <>); + if ($coeff_elas >= 1.0 || $coeff_elas <= 0) + { + print "COEFFICIENT MUST BE > 0 AND < 1.0\n\n\n"; + next; + } + + print "\nFEET\n"; + $S1 = int(70.0 / ($velocity / (16.0 * $time_inc))); + for my $i (1 .. $S1) + { + $T[$i] = $velocity * $coeff_elas ** ($i - 1) / 16.0; + } + + # draw graph + for (my $height=int(-16.0 * ($velocity / 32.0) ** 2.0 + $velocity ** 2.0 / 32.0 + .5) ; $height >= 0 ; $height -= .5) + { + if (int($height) == $height) { print sprintf("%2d", $height); } + else { print " "; } + $line_pos = 0; + my $curr_line = ""; + for my $i (1 .. $S1) + { + my $time; + for ($time=0 ; $time <= $T[$i] ; $time += $time_inc) + { + $line_pos += $time_inc; + if (abs($height - (.5 * (-32) * $time ** 2.0 + $velocity * $coeff_elas ** ($i - 1) * $time)) <= .25) + { + $curr_line = line_tab($curr_line, ($line_pos / $time_inc), "0"); + } + } + $time = ($T[$i + 1] // 0) / 2; # we can reach 1 past the end, use 0 if that happens + last if (-16.0 * $time ** 2.0 + $velocity * $coeff_elas ** ($i - 1) * $time < $height); + } + print "$curr_line\n"; + } + + # draw scale + print " ."; + print "." x (int($line_pos + 1) / $time_inc + 1), "\n"; + print " 0"; + my $curr_line = ""; + for my $i (1 .. int($line_pos + .9995)) + { + $curr_line = line_tab($curr_line, int($i / $time_inc), $i); + } + print "$curr_line\n"; + print " " x (int($line_pos + 1) / (2 * $time_inc) - 2), "SECONDS\n\n"; +} diff --git a/14_Bowling/perl/README.md b/14_Bowling/perl/README.md index e69c8b81..9e030cb0 100644 --- a/14_Bowling/perl/README.md +++ b/14_Bowling/perl/README.md @@ -1,3 +1,22 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +###Bowling program in Perl + +Run normally, this is a fairly faithful translation of the Basic game. +The only real differences are a few trivial fix-ups on the prints to make it +look better, and the player/frame/ball line was put before the "get the ball +going" line to make it more obvious who's turn it is. + +However, if you run it with "-a" on the command line, it will go into +"advanced" mode, which means that "." is used to show pin down and "!" for +pin up, current running scores are shown at the end of each frame, and the +scoring also looks more normal at the end. This is all done because I think it +looks better and I wanted to see a score. Having a flag says you can play +whichever version of the game you like. + +Note, the original code doesn't do the 10th frame correctly, in that it will +never do more than 2 balls, so the best score you can get is a 290. +This is true in both modes. That being said, it will always give you a mediocre +game; I don't think I've ever seen a score over 140. diff --git a/14_Bowling/perl/bowling.pl b/14_Bowling/perl/bowling.pl new file mode 100755 index 00000000..1a00cd6b --- /dev/null +++ b/14_Bowling/perl/bowling.pl @@ -0,0 +1,254 @@ +#!/usr/bin/perl + +# Bowling program in Perl +# Run normally, this is a fairly faithful translation of the Basic game. +# The only real differences are a few trivial fix-ups on the prints to make it +# look better, and the player/frame/ball line was put before the "get the ball +# going" line to make it more obvious who's turn it is. +# +# However, if you run it with "-a" on the command line, it will go into +# 'advanced' mode, which means that "." is used to show pin down and "!" for +# pin up, current running scores are shown at the end of each frame, and the +# scoring also looks more normal at the end. This is all done because I think it +# looks better and I wanted to see a score. Having a flag says you can play +# whichever version of the game you like. +# +# Note, the original code doesn't do the 10th frame correctly, in that it will +# never do more than 2 balls, so the best score you can get is a 290. +# This is true in both modes. That being said, it will always give you a mediocre game. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +print "\n"; +print " " x 34, "BASKETBALL\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +# globals +my @C; # pin position matraix? +my @Scores; # scores: [player num][frame][ball] # ball3 is end-result, ball4 is frame score (advanced mode) +my $Answer; # get answers +my $Num_players; # number of players +my $Advanced = 0; # flag, 1 to use advanced code, 0 for original code +my $char_down = '0'; # char to show pin is down +my $char_up = '+'; # char to show pin is standing + +if ($ARGV[0] && $ARGV[0] eq "-a") +{ + shift; + $Advanced = 1; + $char_down = '.'; + $char_up = '!'; +} + +print "WELCOME TO THE ALLEY\n"; +print "BRING YOUR FRIENDS\n"; +print "OKAY LET'S FIRST GET ACQUAINTED\n\n"; +print "SEE THE INSTRUCTIONS (Y/N): "; +chomp($Answer = uc(<>)); +if ($Answer eq "Y") +{ + print "THE GAME OF BOWLING TAKES MIND AND SKILL. DURING THE GAME\n"; + print "THE COMPUTER WILL KEEP SCORE.YOU MAY COMPETE WITH\n"; + print "OTHER PLAYERS[UP TO FOUR]. YOU WILL BE PLAYING TEN FRAMES\n"; + print "ON THE PIN DIAGRAM 'O' MEANS THE PIN IS DOWN...'+' MEANS THE\n"; + print "PIN IS STANDING. AFTER THE GAME THE COMPUTER WILL SHOW YOUR SCORES.\n"; +} + +do { + print "FIRST OF ALL...HOW MANY ARE PLAYING (1-4): "; + $Num_players = int(<>); +} while ($Num_players < 1 || $Num_players > 4); + +print "\nVERY GOOD...\n"; + +while (1) +{ + # reset all scores + for my $p (1 .. $Num_players) # players + { + for my $f (1 .. 10) # frames + { + for my $b (1 .. 3) # balls + { + $Scores[$p][$f][$b] = 0; + } + } + } + + # play the game + for my $frame (1 .. 10) # frame + { + for my $curr_player (1 .. $Num_players) # player + { + my $last_pins=0; # pins down for last ball + my $ball=1; # ball number, 1 or 2 + my $end_result=0; # result at end of turn: 3=strike, 2=spare, 1=pins-left + for my $i (1 .. 15) { $C[$i] = 0 } + + while (1) # another ball + { + # ARK BALL GENERATOR USING MOD '15' SYSTEM + my $K=0; + my $curr_pins=0; # pins down for this ball + for my $i (1 .. 20) + { + my $x = int(rand(1) * 100); + my $j; + for ($j=1 ; $j <= 10 ; $j++) + { + last if ($x < 15 * $j); + } + $C[15 * $j - $x] = 1; + } + + # ARK PIN DIAGRAM + print "PLAYER: $curr_player FRAME: $frame BALL: $ball\n"; + print "PRESS ENTER TO GET THE BALL GOING."; + $Answer = <>; # not used, just need an enter + for my $i (0 .. 3) + { + print "\n"; + print " " x $i; # avoid the TAB(), just shift each row over for the triangle + for my $j (1 .. 4 - $i) + { + $K++; + print ($C[$K] == 1 ? " $char_down" : " $char_up"); + } + } + print "\n"; + + # ARK ROLL ANALYSIS + for my $i (1 .. 10) + { + $curr_pins += $C[$i]; + } + if ($curr_pins - $last_pins == 0) + { + print "GUTTER!!\n"; + } + if ($ball == 1 && $curr_pins == 10) + { + print "STRIKE!!!!!\a\a\a\a\n"; # \a is for bell + $end_result = 3; + } + elsif ($ball == 2 && $curr_pins == 10) + { + print "SPARE!!!!\n"; + $end_result = 2; + } + elsif ($ball == 2 && $curr_pins < 10) + { + if ($Advanced) { print 10 - $curr_pins, " PENS LEFT!!!\n"; } + else { print "ERROR!!!\n"; } + $end_result = 1; + } + if ($ball == 1 && $curr_pins < 10) + { + print "ROLL YOUR 2ND BALL\n"; + } + print "\n"; + + # ARK STORAGE OF THE SCORES + if ($Advanced) { $Scores[$curr_player][$frame][$ball] = $curr_pins - $last_pins; } + else { $Scores[$curr_player][$frame][$ball] = $curr_pins; } + if ($ball == 1) + { + $ball = 2; + $last_pins = $curr_pins; + + if ($end_result == 3) # strike, no more rolls, goto last + { + $Scores[$curr_player][$frame][$ball] = $curr_pins; + } + else + { + $Scores[$curr_player][$frame][$ball] = $curr_pins - $last_pins; + next if ($end_result == 0); # next roll + } + } + last; + } + $Scores[$curr_player][$frame][3] = $end_result; + } # next player + if ($Advanced) + { + print "Scores:\n"; + for my $p (1 .. $Num_players) + { + my $total = calc_score($p); + print "\tPlayer $p: $total\n"; + } + print "\n"; + } + } # next frame + + # end of game, show full scoreboard + show_scoreboard(); + + print "DO YOU WANT ANOTHER GAME (Y/N): "; + chomp($Answer = uc(<>)); + print "\n"; + last if ($Answer ne "Y"); +} +exit(0); + +sub show_scoreboard +{ + print "FRAMES\n"; + for my $i (1 .. 10) + { + print " $i "; + } + print "\n"; + my @results = ( "-", ".", "/", "X" ); + for my $p (1 .. $Num_players) + { + print "Player $p\n" if ($Advanced); + my $ball_max = ($Advanced ? 4 : 3); + for my $b (1 .. $ball_max) + { + for my $f (1 .. 10) + { + if ($b != 3) { print sprintf("%2d ", $Scores[$p][$f][$b]); } + else { print sprintf("%2s ", $results[$Scores[$p][$f][$b]]); } + } + print "\n"; + } + print "\n"; + } +} + +sub calc_score +{ + my $player = shift; + my $total = 0; + for my $frame (1 .. 10) + { + my $score = 0; + if ($frame == 10 || $Scores[$player][$frame][3] == 1) # pins + { + $score = $Scores[$player][$frame][1] + $Scores[$player][$frame][2]; + } + elsif ($Scores[$player][$frame][3] == 2) # spare + { + $score = 10 + $Scores[$player][$frame+1][1]; + } + elsif ($Scores[$player][$frame][3] == 3) # strike + { + $score = 10 + $Scores[$player][$frame+1][1]; + if ($Scores[$player][$frame+1][1] == 10) + { + $score += ($frame < 9 ? $Scores[$player][$frame+2][1] : $Scores[$player][$frame+1][2]); + } + else + { + $score += $Scores[$player][$frame+1][2]; + } + } + $Scores[$player][$frame][4] = $score; + $total += $score; + } + return $total; +} diff --git a/15_Boxing/perl/boxing.pl b/15_Boxing/perl/boxing.pl new file mode 100644 index 00000000..5068e58a --- /dev/null +++ b/15_Boxing/perl/boxing.pl @@ -0,0 +1,252 @@ +#!/usr/bin/perl + +# Boxing program in Perl +# Required extensive restructuring to remove all of the GOTO's. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $Opp_won = 0; # num rounds opponent has won +my $You_won = 0; # num rounds you have won +my $Opp_name = ""; # opponent name +my $Your_name = ""; # your name +my $Your_best = 0; # your best punch +my $Your_worst = 0; # your worst punch +my $Opp_best; # opponent best punch +my $Opp_worst; # opponent worst punch +my $Opp_damage; # opponent damage ? +my $Your_damage; # your damage ? + +sub get_punch +{ + my $prompt = shift; + my $p; + while (1) + { + print "$prompt: "; + chomp($p = int(<>)); + last if ($p >= 1 && $p <= 4); + print "DIFFERENT PUNCHES ARE: (1) FULL SWING; (2) HOOK; (3) UPPERCUT; (4) JAB.\n"; + } + return $p; +} + +print "\n"; +print " " x 33, "BOXING\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +print "BOXING OLYMPIC STYLE (3 ROUNDS -- 2 OUT OF 3 WINS)\n\n"; +print "WHAT IS YOUR OPPONENT'S NAME: "; +chomp($Opp_name = <>); +print "INPUT YOUR MAN'S NAME: "; +chomp($Your_name = <>); +print "DIFFERENT PUNCHES ARE: (1) FULL SWING; (2) HOOK; (3) UPPERCUT; (4) JAB.\n"; +$Your_best = get_punch("WHAT IS YOUR MANS BEST"); +$Your_worst = get_punch("WHAT IS HIS VULNERABILITY"); + +do { + $Opp_best = int(4*rand(1)+1); + $Opp_worst = int(4*rand(1)+1); +} while ($Opp_best == $Opp_worst); +print "$Opp_name\'S ADVANTAGE IS $Opp_best AND VULNERABILITY IS SECRET.\n\n"; + +for my $R (1 .. 3) # rounds +{ + last if ($Opp_won >= 2 || $You_won >= 2); + $Opp_damage = 0; + $Your_damage = 0; + print "ROUND $R BEGINS...\n"; + for my $R1 (1 .. 7) # 7 events per round? + { + if (int(10*rand(1)+1) <= 5) + { + my $your_punch = get_punch("$Your_name\'S PUNCH"); + $Opp_damage += 2 if ($your_punch == $Your_best); + + if ($your_punch == 1) { punch1(); } + elsif ($your_punch == 2) { punch2(); } + elsif ($your_punch == 3) { punch3(); } + else { punch4(); } + next; + } + + my $Opp_punch = int(4*rand(1)+1); + $Your_damage += 2 if ($Opp_punch == $Opp_best); + + if ($Opp_punch == 1) { opp1(); } + elsif ($Opp_punch == 2) { opp2(); } + elsif ($Opp_punch == 3) { opp3(); } + else { opp4(); } + } + + if ($Opp_damage > $Your_damage) + { + print "\n$Your_name WINS ROUND $R\n\n"; + $You_won++; + } + else + { + print "\n$Opp_name WINS ROUND $R\n\n"; + $Opp_won++; + } +} + +if ($Opp_won >= 2) +{ + done("$Opp_name WINS (NICE GOING, $Opp_name)."); +} + +#else # if ($You_won >= 2) +done("$Your_name AMAZINGLY WINS!!"); + +################################################### + +sub done +{ + my $msg = shift; + print $msg; + print "\n\nAND NOW GOODBYE FROM THE OLYMPIC ARENA.\n\n"; + exit(0); +} + +sub punch1 +{ + # $your_punch == 1, full swing + print "$Your_name SWINGS AND "; + if ($Opp_worst == 4 || int(30*rand(1)+1) < 10) + { + print "HE CONNECTS!\n"; + if ($Opp_damage > 35) + { + done("$Opp_name IS KNOCKED COLD AND $Your_name IS THE WINNER AND CHAMP! "); + } + $Opp_damage += 15; + } + else + { + print "HE MISSES\n"; + print "\n\n" if ($Opp_damage != 1); + } +} + +sub punch2 +{ + # $your_punch == 2, hook + print "$Your_name GIVES THE HOOK... "; + if ($Opp_worst == 2) + { + $Opp_damage += 7; + return; + } + if (int(2*rand(1)+1) == 1) + { + print "BUT IT'S BLOCKED!!!!!!!!!!!!!\n"; + } + else + { + print "CONNECTS...\n"; + $Opp_damage += 7; + } +} + +sub punch3 +{ + # $your_punch == 3, uppercut + print "$Your_name TRIES AN UPPERCUT "; + if ($Opp_worst == 3 || int(100*rand(1)+1) < 51) + { + print "AND HE CONNECTS!\n"; + $Opp_damage += 4; + } + else + { + print "AND IT'S BLOCKED (LUCKY BLOCK!)\n"; + } +} + +sub punch4 +{ + # $your_punch == 4, jab + print "$Your_name JABS AT $Opp_name\'S HEAD "; + if ($Opp_worst == 4 || (int(8*rand(1)+1)) >= 4) + { + $Opp_damage += 3; + print "\n"; + } + else + { + print "IT'S BLOCKED.\n"; + } +} + +sub opp1 +{ + # opp_punch == 1 + print "$Opp_name TAKES A FULL SWING AND "; + if ($Your_worst == 1 || int(60*rand(1)+1) < 30) + { + print " POW!!!!! HE HITS HIM RIGHT IN THE FACE!\n"; + if ($Your_damage > 35) + { + done("$Your_name IS KNOCKED COLD AND $Opp_name IS THE WINNER AND CHAMP!"); + } + $Your_damage += 15; + } + else + { + print " IT'S BLOCKED!\n"; + } +} + +sub opp2 +{ + # opp_punch == 2 + print "$Opp_name GETS $Your_name IN THE JAW (OUCH!)\n"; + $Your_damage += 7; + print "....AND AGAIN!\n"; + $Your_damage += 5; + if ($Your_damage > 35) + { + done("$Your_name IS KNOCKED COLD AND $Opp_name IS THE WINNER AND CHAMP!"); + } + print "\n"; + # 2 continues into opp_punch == 3 + opp3(); +} + +sub opp3() +{ + # opp_punch == 3 + print "$Your_name IS ATTACKED BY AN UPPERCUT (OH,OH)...\n"; + if ($Your_worst != 3 && int(200*rand(1)+1) > 75) + { + print " BLOCKS AND HITS $Opp_name WITH A HOOK.\n"; + $Opp_damage += 5; + } + else + { + print "AND $Opp_name CONNECTS...\n"; + $Your_damage += 8; + } +} + +sub opp4 +{ + # opp_punch == 4 + print "$Opp_name JABS AND "; + if ($Your_worst == 4) + { + $Your_damage += 5; + } + elsif (int(7*rand(1)+1) > 4) + { + print " BLOOD SPILLS !!!\n"; + $Your_damage += 5; + } + else + { + print "IT'S BLOCKED!\n"; + } +} diff --git a/23_Checkers/perl/README.md b/23_Checkers/perl/README.md index e69c8b81..4323cf4a 100644 --- a/23_Checkers/perl/README.md +++ b/23_Checkers/perl/README.md @@ -1,3 +1,9 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +Note: This version has lines and columns numbers to help you with choosing the cell +to move from and to, so you don't have to continually count. It also puts a "." only for +blank cells you can move to, which I think makes for a more pleasing look and makes +it easier to play. If you want the original behavior, start the program with an arg +of "-o" for the original behavior. diff --git a/23_Checkers/perl/checkers.pl b/23_Checkers/perl/checkers.pl new file mode 100755 index 00000000..46b8f2d4 --- /dev/null +++ b/23_Checkers/perl/checkers.pl @@ -0,0 +1,351 @@ +#!/usr/bin/perl + +# Checkers program in Perl +# Started with checkers.annotated.bas +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +# +# The current move: (rating, current x, current y, new x, new y) +# 'rating' represents how good the move is; higher is better. +my @ratings = (-99); # (4); # Start with minimum score +# The board. Pieces are represented by numeric values: +# +# - 0 = empty square +# - -1,-2 = X (-1 for regular piece, -2 for king) +# - 1,2 = O (1 for regular piece, 2 for king) +# +# This program's player ("me") plays X. +my @board; # (7,7) +# chars to print for the board, add 2 to the board value as an index to the char +my @chars = ("X*", "X", ".", "O", "O*"); +my $neg1 = -1; # constant holding -1 +my $winner = ""; +my $upgrade = shift(@ARGV) // ""; +$upgrade = $upgrade eq "-o" ? 0 : 1; + +##### + +print "\n"; +print " " x 32, "CHECKERS\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + + +print "THIS IS THE GAME OF CHECKERS. THE COMPUTER IS X,\n"; +print "AND YOU ARE O. THE COMPUTER WILL MOVE FIRST.\n"; +print "SQUARES ARE REFERRED TO BY A COORDINATE SYSTEM.\n"; +print "(0,0) IS THE LOWER LEFT CORNER\n"; +print "(0,7) IS THE UPPER LEFT CORNER\n"; +print "(7,0) IS THE LOWER RIGHT CORNER\n"; +print "(7,7) IS THE UPPER RIGHT CORNER\n"; +print "THE COMPUTER WILL TYPE '+TO' WHEN YOU HAVE ANOTHER\n"; +print "JUMP. TYPE TWO NEGATIVE NUMBERS IF YOU CANNOT JUMP.\n"; +print "ENTER YOUR MOVE POSITION LIKE '0 0' OR '0,0'.\n\n\n"; + +# Initialize the board. Data is 2 length-wise strips repeated. +my @data = (); +for (1 .. 32) { push(@data, (1,0,1,0,0,0,-1,0, 0,1,0,0,0,-1,0,-1)); } +for my $x (0 .. 7) +{ + for my $y (0 .. 7) + { + $board[$x][$y] = shift(@data); + } +} + +# Start of game loop. First, my turn. +while (1) +{ + + # For each square on the board, search for one of my pieces + # and if it can make the best move so far, store that move in 'r' + for my $x (0 .. 7) + { + for my $y (0 .. 7) + { + # Skip if this is empty or an opponent's piece + next if ($board[$x][$y] > -1); + + # If this is one of my ordinary pieces, analyze possible + # forward moves. + if ($board[$x][$y] == -1) + { + for (my $a = -1 ; $a <= 1 ; $a +=2) + { + $b = $neg1; + find_move($x, $y, $a, $b); + } + } + + # If this is one of my kings, analyze possible forward + # and backward moves. + if ($board[$x][$y] == -2) + { + for (my $a = -1 ; $a <= 1 ; $a += 2) + { + for (my $b = -1 ; $a <= 1 ; $b += 2) { find_move($x, $y, $a, $b); } + } + } + } + } + + + if ($ratings[0] == -99) # Game is lost if no move could be found. + { + $winner = "you"; + last; + } + + # Print the computer's move. (Note: chr$(30) is an ASCII RS + # (record separator) code; probably no longer relevant.) + print "FROM $ratings[1],$ratings[2] TO $ratings[3],$ratings[4] "; + $ratings[0] = -99; + + # Make the computer's move. If the piece finds its way to the + # end of the board, crown it. + LOOP1240: { + if ($ratings[4] == 0) + { + $board[$ratings[3]][$ratings[4]] = -2; + last LOOP1240; + } + $board[$ratings[3]][$ratings[4]] = $board[$ratings[1]][$ratings[2]]; + $board[$ratings[1]][$ratings[2]] = 0; + + # If the piece has jumped 2 squares, it means the computer has + # taken an opponents' piece. + if (abs($ratings[1] - $ratings[3]) == 2) + { + $board [($ratings[1]+$ratings[3])/2] [($ratings[2]+$ratings[4])/2] = 0; # Delete the opponent's piece + + # See if we can jump again. Evaluate all possible moves. + my $x = $ratings[3]; + my $y = $ratings[4]; + for (my $a = -2 ; $a <= 2 ; $a += 4) + { + if ($board[$x][$y] == -1) + { + $b = -2; + eval_move($x, $y, $a, $b); + } + if ($board[$x][$y] == -2) + { + for (my $b = -2 ; $b <= 2 ; $b += 4) { eval_move($x, $y, $a, $b); } + } + } + + # If we've found a move, go back and make that one as well + if ($ratings[0] != -99) + { + print "TO $ratings[3], $ratings[4] "; + $ratings[0] = -99; + next LOOP1240; + } + } + } # LOOP1240 + + # Now, print the board + print "\n\n\n"; + for (my $y = 7 ; $y >= 0 ; $y--) + { + my $line = ""; + $line = "$y|" if ($upgrade); + for my $x (0 .. 7) + { + my $c = $chars[$board[$x][$y] + 2]; + $c = ' ' if ($upgrade && (($y % 2 == 0 && $x % 2 == 1) || ($y % 2 == 1 && $x % 2 == 0))); + $line = tab($line, 5*$x+7, $c); + } + print $line; + print " \n\n"; + } + print " _ _ _ _ _ _ _ _\n" if ($upgrade); + print " 0 1 2 3 4 5 6 7\n" if ($upgrade); + print "\n"; + + # Check if either player is out of pieces. If so, announce the + # winner. + my ($z, $t) = (0, 0); + for my $x (0 .. 7) + { + for my $y (0 .. 7) + { + if ($board[$x][$y] == 1 || $board[$x][$y] == 2) { $z = 1; } + if ($board[$x][$y] == -1 || $board[$x][$y] == -2) { $t = 1; } + } + } + if ($z != 1) { $winner = "comp"; last; } + if ($t != 1) { $winner = "you"; last; } + + # Prompt the player for their move. + ($z, $t) = (0, 0); + my ($x, $y, $e, $h, $a, $b); + do { + ($e,$h) = get_pos("FROM:"); + $x = $e; + $y = $h; + } while ($board[$x][$y] <= 0); + do { + ($a,$b) = get_pos("TO:"); + $x = $a; + $y = $b; + } while (!($board[$x][$y] == 0 && abs($a-$e) <= 2 && abs($a-$e) == abs($b-$h))); + + LOOP1750: { + # Make the move and stop unless it might be a jump. + $board[$a][$b] = $board[$e][$h]; + $board[$e][$h] = 0; + if (abs($e-$a) != 2) { last LOOP1750; } + + # Remove the piece jumped over + $board[($e+$a)/2][($h+$b)/2] = 0; + + # Prompt for another move; -1 means player can't, so I've won. + # Keep prompting until there's a valid move or the player gives + # up. + my ($a1, $b1); + do { + ($a1,$b1) = get_pos("+TO:"); + if ($a1 < 0) { last LOOP1750; } + } while ($board[$a1][$b1] != 0 || abs($a1-$a) != 2 || abs($b1-$b) != 2); + + # Update the move variables to correspond to the next jump + $e = $a; + $h = $b; + $a = $a1; + $b = $b1; + } + + # If the player has reached the end of the board, crown this piece + if ($b == 7) { $board[$a][$b] = 2; } + + # And play the next turn. +} + +# Endgame: +print "\n", ($winner eq "you" ? "YOU" : "I"), " WIN\n"; +exit(0); + +########################################### + +# make sure we get a 2 value position +sub get_pos +{ + my $prompt = shift; + my ($p1, $p2); + do { + print "$prompt "; + chomp(my $ans = <>); + ($p1,$p2) = split(/[, ]/, $ans); + } while (!defined($p1) || !defined($p2) || $p1 < -1 || $p2 < -1 || $p1 > 7 || $p2 > 7); + return ($p1,$p2); +} + +# deal with basic's tab() for line positioning +# line = line string we're starting with +# pos = position to start writing +# s = string to write +# returns the resultant string, which might not have been changed +sub tab +{ + my ($line, $pos, $str) = @_; + my $len = length($line); + # if curser is past position, do nothing + if ($len <= $pos) { $line .= " " x ($pos - $len) . $str; } + return $line; +} + +# Analyze a move from (x,y) to (x+a, y+b) and schedule it if it's +# the best candidate so far. +sub find_move +{ + my ($x, $y, $a, $b) = @_; + my $u = $x+$a; + my $v = $y+$b; + + # Done if it's off the board + return if ($u < 0 || $u > 7 || $v < 0 || $ v> 7); + + # Consider the destination if it's empty + eval_jump($x, $y, $u, $v) if ($board[$u][$v] == 0); + + # If it's got an opponent's piece, jump it instead + if ($board[$u][$v] > 0) + { + + # Restore u and v, then return if it's off the board + $u += $a; + $v += $b; + return if ($u < 0 || $v < 0 || $u > 7 || $v > 7); + + # Otherwise, consider u,v + eval_jump($x, $y, $u, $v) if ($board[$u][$v] == 0); + } +} + +# Evaluate jumping (x,y) to (u,v). +# +# Computes a score for the proposed move and if it's higher +# than the best-so-far move, uses that instead by storing it +# and its score in @ratings. +sub eval_jump +{ + my ($x, $y, $u, $v) = @_; + + # q is the score; it starts at 0 + my $q = 0; + + # +2 if it promotes this piece + $q += 2 if ($v == 0 && $board[$x][$y] == -1); + + # +5 if it takes an opponent's piece + $q += 5 if (abs($y-$v) == 2); + + # -2 if the piece is moving away from the top boundary + $q -= 2 if ($y == 7); + + # +1 for putting the piece against a vertical boundary + $q++ if ($u == 0 || $u == 7); + + for (my $c = -1 ; $c <= 1 ; $c += 2) + { + next if ($u+$c < 0 || $u+$c > 7 || $v+$neg1 < 0); + + # +1 for each adjacent friendly piece + if ($board[$u+$c][$v+$neg1] < 0) + { + $q++; + next; + } + + # Prevent out-of-bounds testing + next if ($u-$c < 0 || $u-$c > 7 || $v-$neg1 > 7); + + # -2 for each opponent piece that can now take this piece here + $q -= 2 if ($board[$u+$c][$v+$neg1] > 0 && ($board[$u-$c][$v-$neg1] == 0 || ($u-$c == $x && $v-$neg1 == $y))); + } + + # Use this move if it's better than the previous best + if ($q > $ratings[0]) + { + $ratings[0] = $q; + $ratings[1] = $x; + $ratings[2] = $y; + $ratings[3] = $u; + $ratings[4] = $v; + } +} + +# If (u,v) is in the bounds, evaluate it as a move using +# the sub at 910, so storing eval in @ratings. +sub eval_move +{ + my ($x, $y, $a, $b) = @_; + my $u = $x+$a; + my $v = $y+$b; + return if ($u < 0 || $u > 7 || $v < 0 || $v > 7); + eval_jump($x, $y, $u, $v) if ($board[$u][$v] == 0 && $board[$x+$a/2][$y+$b/2] > 0); +} diff --git a/28_Combat/perl/combat.pl b/28_Combat/perl/combat.pl new file mode 100755 index 00000000..fc44a0e4 --- /dev/null +++ b/28_Combat/perl/combat.pl @@ -0,0 +1,202 @@ +#!/usr/bin/perl + +# Combat program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $User_army; +my $User_navy; +my $User_AF; +my $Comp_army = 30000; +my $Comp_navy = 20000; +my $Comp_AF = 22000; +my $Attack_type; +my $Attack_num; + +print "\n"; +print " " x 33, "COMBAT\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +print "I AM AT WAR WITH YOU.\nWE HAVE 72000 SOLDIERS APIECE.\n\n"; + +do { + print "DISTRIBUTE YOUR FORCES.\n"; + print "\tME\t YOU\n"; + print "ARMY\t$Comp_army\t"; + chomp($User_army = <>); + print "NAVY\t$Comp_navy\t"; + chomp($User_navy = <>); + print "A. F.\t$Comp_AF\t"; + chomp($User_AF = <>); +} while ($User_army + $User_navy + $User_AF > 72000); + +do { + print "YOU ATTACK FIRST. TYPE (1) FOR ARMY; (2) FOR NAVY;\n"; + print "AND (3) FOR AIR FORCE.\n"; + chomp($Attack_num = <>); +} while ($Attack_type < 1 && $Attack_type > 3); +do { + print "HOW MANY MEN\n"; + chomp($Attack_type = <>); +} while ($Attack_type < 0 + || ($Attack_num == 1 && $Attack_type > $User_army) + || ($Attack_num == 2 && $Attack_type > $User_navy) + || ($Attack_num == 3 && $Attack_type > $User_AF)); + +if ($Attack_num == 1) +{ + if ($Attack_type<$User_army/3) + { + print "YOU LOST $Attack_type MEN FROM YOUR ARMY.\n"; + $User_army = int($User_army-$Attack_type); + } + if ($Attack_type<2*$User_army/3) + { + print "YOU LOST ", int($Attack_type/3), " MEN, BUT I LOST ", int(2*$Comp_army/3), "\n"; + $User_army = int($User_army-$Attack_type/3); + $Comp_army = int(2*$Comp_army/3); + } + else + { + s270(); + } +} +elsif ($Attack_num == 2) +{ + if ($Attack_type < $Comp_navy/3) + { + print "YOUR ATTACK WAS STOPPED!\n"; + $User_navy = int($User_navy-$Attack_type); + } + if ($Attack_type < 2*$Comp_navy/3) + { + print "YOU DESTROYED ", int(2*$Comp_navy/3), " OF MY ARMY.\n"; + $Comp_navy = int(2*$Comp_navy/3); + } + else + { + s270(); + } +} +else # $Attack_num == 3 +{ + if ($Attack_type < $User_AF/3) + { + print "YOUR ATTACK WAS WIPED OUT.\n"; + $User_AF = int($User_AF-$Attack_type); + } + if ($Attack_type < 2*$User_AF/3) + { + print "WE HAD A DOGFIGHT. YOU WON - AND FINISHED YOUR MISSION.\n"; + $Comp_army = int(2*$Comp_army/3); + $Comp_navy = int($Comp_navy/3); + $Comp_AF = int($Comp_AF/3); + } + else + { + print "YOU WIPED OUT ONE OF MY ARMY PATROLS, BUT I DESTROYED\n"; + print "TWO NAVY BASES AND BOMBED THREE ARMY BASES.\n"; + $User_army = int($User_army/4); + $User_navy = int($User_navy/3); + $User_AF = int(2*$User_AF/3); + } +} + +print "\n\tYOU\tME\n"; +print "ARMY\t$User_army\t$Comp_army\n"; +print "NAVY\t$User_navy\t$Comp_navy\n"; +print "A. F.\t$User_AF\t$Comp_AF\n"; +do { + print "WHAT IS YOUR NEXT MOVE?\n"; + print "ARMY=1 NAVY=2 AIR FORCE=3\n"; + chomp($Attack_type = <>); +} while ($Attack_type < 1 && $Attack_type > 3); +do { + print "HOW MANY MEN\n"; + chomp($Attack_num = <>); +} while ($Attack_num < 0 + || ($Attack_type == 1 && $Attack_num > $User_army) + || ($Attack_type == 2 && $Attack_num > $User_navy) + || ($Attack_type == 3 && $Attack_num > $User_AF)); + +if ($Attack_num == 1) +{ + if ($Attack_num < $Comp_army/2) + { + print "I WIPED OUT YOUR ATTACK!\n"; + $User_army -= $Attack_num; + } + else + { + print "YOU DESTROYED MY ARMY!\n"; + $Comp_army = 0; + } +} +elsif ($Attack_num == 2) +{ + if ($Attack_num < $Comp_navy/2) + { + print "I SUNK TWO OF YOUR BATTLESHIPS, AND MY AIR FORCE\n"; + print "WIPED OUT YOUR UNGAURDED CAPITOL.\n"; + $User_army /= 4; + $User_navy /= 2; + } + else + { + print "YOUR NAVY SHOT DOWN THREE OF MY XIII PLANES,\n"; + print "AND SUNK THREE BATTLESHIPS.\n"; + $Comp_AF = 2*$Comp_AF/3; + $Comp_navy /= 2; + } +} +else # $Attack_num == 3 +{ + if ($Attack_num > $Comp_AF/2) + { + print "MY NAVY AND AIR FORCE IN A COMBINED ATTACK LEFT\n"; + print "YOUR COUNTRY IN SHAMBLES.\n"; + $User_army /= 3; + $User_navy /= 3; + $User_AF /= 3; + } + else + { + print "ONE OF YOUR PLANES CRASHED INTO MY HOUSE. I AM DEAD.\n"; + print "MY COUNTRY FELL APART.\n"; + $Comp_army = $Comp_navy = $Comp_AF = 0; + } +} + +print "\nFROM THE RESULTS OF BOTH OF YOUR ATTACKS,\n"; +my $total_user = $User_army+$User_navy+$User_AF; +my $total_comp = $Comp_army+$Comp_navy+$Comp_AF; +if ($total_user > 3/2*($total_comp)) +{ + print "YOU WON, OH! SHUCKS!!!!\n"; +} +elsif ($total_user < 2/3*($total_comp)) +{ + print "YOU LOST-I CONQUERED YOUR COUNTRY. IT SERVES YOU\n"; + print "RIGHT FOR PLAYING THIS STUPID GAME!!!\n"; +} +else +{ + print "THE TREATY OF PARIS CONCLUDED THAT WE TAKE OUR\n"; + print "RESPECTIVE COUNTRIES AND LIVE IN PEACE.\n"; +} +print "\n"; +exit(0); + +####################################################### + +sub s270 +{ + print "YOU SUNK ONE OF MY PATROL BOATS, BUT I WIPED OUT TWO\n"; + print "OF YOUR AIR FORCE BASES AND 3 ARMY BASES.\n"; + $User_army = int($User_army/3); + $User_AF = int($User_AF/3); + $Comp_navy = int(2*$Comp_navy/3); +} diff --git a/34_Digits/perl/digits.pl b/34_Digits/perl/digits.pl new file mode 100755 index 00000000..2f49bd0e --- /dev/null +++ b/34_Digits/perl/digits.pl @@ -0,0 +1,125 @@ +#!/usr/bin/perl + +# Digits program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $Answer; + +print "\n"; +print " " x 33, "DIGITS"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +print "THIS IS A GAME OF GUESSING.\n"; +print "FOR INSTRUCTIONS, TYPE '1', ELSE TYPE '0': "; +chomp($Answer = <>); +if ($Answer == 1) +{ + print "\nPLEASE TAKE A PIECE OF PAPER AND WRITE DOWN\n"; + print "THE DIGITS '0', '1', OR '2' THIRTY TIMES AT RANDOM.\n"; + print "ARRANGE THEM IN THREE LINES OF TEN DIGITS EACH.\n"; + print "I WILL ASK FOR THEN TEN AT A TIME.\n"; + print "I WILL ALWAYS GUESS THEM FIRST AND THEN LOOK AT YOUR\n"; + print "NEXT NUMBER TO SEE IF I WAS RIGHT. BY PURE LUCK,\n"; + print "I OUGHT TO BE RIGHT TEN TIMES. BUT I HOPE TO DO BETTER\n"; + print "THAN THAT. *****\n\n\n"; +} + +my ($A, $B, $C) = (0, 1, 3); +my (@M, @K, @L); # DIM M(26,2),K(2,2),L(8,2) +while (1) +{ + for my $i (0 .. 26) { for my $j (0 .. 2) { $M[$i][$j] = 1; } } + for my $i (0 .. 2) { for my $j (0 .. 2) { $K[$i][$j] = 9; } } + for my $i (0 .. 8) { for my $j (0 .. 2) { $L[$i][$j] = 3; } } + $L[0][0] = $L[4][1] = $L[8][2] = 2; + my $Z = 26; + my $Z1 = 8; + my $Z2 = 2; + my $X = 0; + my @N; + + for my $T (1 .. 3) + { + my $have_input = 0; + while (!$have_input) + { + $have_input = 1; + print "\nTEN NUMBERS, PLEASE: "; + chomp($Answer = <>); + $Answer = "0 " . $Answer; # need to be 1-based, so prepend a throw-away value for [0] + @N = split(/\s+/, $Answer); + for my $i (1 .. 10) + { + if (!defined($N[$i]) || ($N[$i] != 0 && $N[$i] != 1 && $N[$i] != 2)) + { + print "ONLY USE THE DIGITS '0', '1', OR '2'.\n"; + print "LET'S TRY AGAIN."; + $have_input = 0; + last; + } + } + } + + print "\nMY GUESS\tYOUR NO.\tRESULT\tNO. RIGHT\n\n"; + for my $U (1 .. 10) + { + my $num = $N[$U]; + my $S = 0; + my $G; + for my $J (0 .. 2) + { + my $S1 = $A * $K[$Z2][$J] + $B * $L[$Z1][$J] + $C * $M[$Z][$J]; + next if ($S > $S1); + if ($S >= $S1) + { + next if (rand(1) < .5); + } + $S = $S1; + $G = $J; + } # NEXT J + print " $G\t\t$N[$U]\t\t"; + if ($G == $N[$U]) + { + $X++; + print "RIGHT\t$X\n"; + $M[$Z][$num]++; + $L[$Z1][$num]++; + $K[$Z2][$num]++; + $Z -= int($Z / 9) * 9; + $Z = 3 * $Z + $N[$U]; + } + else + { + print "WRONG\t$X\n"; + } + $Z1 = $Z - int($Z / 9) * 9; + $Z2 = $N[$U]; + } # NEXT U + } # NEXT T + + print "\n"; + if ($X == 10) + { + print "I GUESSED EXACTLY 1/3 OF YOUR NUMBERS.\n"; + print "IT'S A TIE GAME.\n"; + } + elsif ($X > 10) + { + print "I GUESSED MORE THAN 1/3, OR $X, OF YOUR NUMBERS.\n"; + print "I WIN.\a\a\a\a\a\a\a\a\a\a" + } + else + { + print "I GUESSED LESS THAN 1/3, OR $X, OF YOUR NUMBERS.\n"; + print "YOU BEAT ME. CONGRATULATIONS *****\n"; + } + + print "\nDO YOU WANT TO TRY AGAIN (1 FOR YES, 0 FOR NO): "; + chomp($Answer = <>); + last if ($Answer != 1); +} +print "\nTHANKS FOR THE GAME.\n"; diff --git a/36_Flip_Flop/perl/flipflop.pl b/36_Flip_Flop/perl/flipflop.pl new file mode 100755 index 00000000..63e3f84e --- /dev/null +++ b/36_Flip_Flop/perl/flipflop.pl @@ -0,0 +1,124 @@ +#!/usr/bin/perl + +# Flip Flop program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; +use Math::Trig; + +print "\n"; +print " " x 32, "FLIPFLOP"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; +# *** CREATED BY MICHAEL CASS + +print "THE OBJECT OF THIS PUZZLE IS TO CHANGE THIS:\n\n"; +print "X X X X X X X X X X\n\n"; +print "TO THIS:\n\n"; +print "O O O O O O O O O O\n\n"; +print "BY TYPING THE NUMBER CORRESPONDING TO THE POSITION OF THE\n"; +print "LETTER ON SOME NUMBERS, ONE POSITION WILL CHANGE, ON\n"; +print "OTHERS, TWO WILL CHANGE. TO RESET LINE TO ALL X'S, TYPE 0\n"; +print "(ZERO) AND TO START OVER IN THE MIDDLE OF A GAME, TYPE \n"; +print "11 (ELEVEN).\n\n"; + +sub initialize +{ + my @a; + print "1 2 3 4 5 6 7 8 9 10\n"; + print "X X X X X X X X X X\n"; + for my $i (0 .. 10) { $a[$i] = "X"; } # make sure [0] has a value just in case + return @a; +} + +while (1) +{ + my $Q = rand(1); + my $C = 0; + print "HERE IS THE STARTING LINE OF X'S.\n\n"; + my @A = initialize(); + + while (1) + { + my $M = 0; + my $N; + while (1) + { + print "\nINPUT THE NUMBER: "; + chomp($N = <>); + if ($N != int($N) || $N < 0 || $N > 11) + { + print "ILLEGAL ENTRY--TRY AGAIN.\n"; + next; + } + last; + } + if ($N == 11) # start a new game + { + print "\n\n"; + last; + } + if ($N == 0) # reset line + { + @A = initialize(); + next; + } + + if ($M != $N) + { + $M = $N; + $A[$N] = ($A[$N] eq "O") ? "X" : "O"; + while ($M == $N) + { + my $R = tan($Q + $N / $Q - $N) - sin($Q / $N) + 336 * sin(8 * $N); + $N = $R - int($R); + $N = int(10 * $N); + if ($A[$N] eq "O") + { + $A[$N] = "X"; + next; + } + $A[$N] = "O"; + last; # GOTO 610 + + $A[$N] = "X"; + } + } + else + { + if ($A[$N] ne "O") { $A[$N] = "O"; } + while ($M == $N) + { + my $R = .592 * (1 / tan($Q / $N + $Q)) / sin( $N * 2 + $Q) - cos($N); + $N = $R - int($R); + $N = int(10 * $N); + if ($A[$N] eq "O") + { + $A[$N] = "X"; + next; + } + $A[$N] = "O"; + last; + } + } + + print "1 2 3 4 5 6 7 8 9 10\n"; + for my $i (1 .. 10) { print "$A[$i] "; } + print "\n"; + $C++; + my $i; + for ($i=1 ; $i <= 10 ; $i++) { + last if ($A[$i] ne "O"); + } + if ($i == 11) + { + if ($C <= 12) { print "VERY GOOD. YOU GUESSED IT IN ONLY $C GUESSES.\n"; } + else { print "TRY HARDER NEXT TIME. IT TOOK YOU $C GUESSES.\n"; } + last; + } + } + print "DO YOU WANT TO TRY ANOTHER PUZZLE (Y/N): "; + $_ = <>; + print "\n"; + last if (m/^n/i); +} diff --git a/38_Fur_Trader/perl/README.md b/38_Fur_Trader/perl/README.md index e69c8b81..c7001be7 100644 --- a/38_Fur_Trader/perl/README.md +++ b/38_Fur_Trader/perl/README.md @@ -1,3 +1,5 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +You can answer yes/no questions in lower case if desired. diff --git a/38_Fur_Trader/perl/furtrader.pl b/38_Fur_Trader/perl/furtrader.pl new file mode 100755 index 00000000..f1e6b01a --- /dev/null +++ b/38_Fur_Trader/perl/furtrader.pl @@ -0,0 +1,265 @@ +#!/usr/bin/perl + +# Fur Trader program in Perl +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my @Pelts = (qw(0 MINK BEAVER ERMINE FOX )); +my $Num_pelts = 4; +my @Quantity; # how many of each fur +my $Money; +my $Max_pelts = 190; +my $Ermine_price; # like we have @Pelts and @Quantity we could have @Prices +my $Beaver_price; # then have 4 constants as index into the arrays to avoid +my $Fox_price; # the magic numbers 1-4, or better have a array of objects (really a hash) +my $Mink_price; # with the 3 keys (name, number, price), but well keep it + # with 4 vars like the basic program did + +print "\n"; +print " " x 31, "FUR TRADER\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +init(); +while (1) +{ + my $ans = get_yesno(); + last if ($ans ne "YES"); + + $Ermine_price = new_price(.15, 0.95); + $Beaver_price = new_price(.25, 1.00); + + print "\nYOU HAVE \$$Money SAVINGS.\n"; + print "AND $Max_pelts FURS TO BEGIN THE EXPEDITION.\n"; + print "\nYOUR $Max_pelts FURS ARE DISTRIBUTED AMONG THE FOLLOWING\n"; + print "KINDS OF PELTS: MINK, BEAVER, ERMINE AND FOX.\n"; + reset_furs(); + + my $total = 0; + for my $j ( 1 .. $Num_pelts) + { + print "\nHOW MANY $Pelts[$j] PELTS DO YOU HAVE? "; + chomp(my $ans = <>); + $Quantity[$j] = int($ans); + $total += $Quantity[$j]; + } + if ($total > $Max_pelts) + { + print "\nYOU MAY NOT HAVE THAT MANY FURS.\n"; + print "DO NOT TRY TO CHEAT. I CAN ADD.\n"; + print "YOU MUST START AGAIN.\n"; + init(); + next; + } + + print "\nYOU MAY TRADE YOUR FURS AT FORT 1, FORT 2,\n"; + print "OR FORT 3. FORT 1 IS FORT HOCHELAGA (MONTREAL)\n"; + print "AND IS UNDER THE PROTECTION OF THE FRENCH ARMY.\n"; + print "FORT 2 IS FORT STADACONA (QUEBEC) AND IS UNDER THE\n"; + print "PROTECTION OF THE FRENCH ARMY. HOWEVER, YOU MUST\n"; + print "MAKE A PORTAGE AND CROSS THE LACHINE RAPIDS.\n"; + print "FORT 3 IS FORT NEW YORK AND IS UNDER DUTCH CONTROL.\n"; + print "YOU MUST CROSS THROUGH IROQUOIS LAND.\n"; + my $done = 0; + while (!$done) + { + $ans = 0; + while ($ans < 1 || $ans > 3) + { + no warnings; # in case user enters alpha chars, then int() will return 0 with no warnings + print "ANSWER 1, 2, OR 3: "; + $ans = int(<>); + } + # returns 0 if they want to go somewhere else; + # or returns the old basic line number to show what to do + if ($ans == 1) { $done = fort1(); } + elsif ($ans == 2) { $done = fort2(); } + elsif ($ans == 3) { $done = fort3(); } + } + + if ($done == 1410) + { + print "YOUR BEAVER SOLD FOR \$", $Beaver_price * $Quantity[2], "\t"; + } + + if ($done <= 1414) + { + print "YOUR FOX SOLD FOR \$", $Fox_price * $Quantity[4], "\n"; + print "YOUR ERMINE SOLD FOR \$", $Ermine_price * $Quantity[3], "\t"; + print "YOUR MINK SOLD FOR \$", $Mink_price * $Quantity[1], "\n"; + } + + # 1418 is always done + $Money += $Mink_price * $Quantity[1] + $Beaver_price * $Quantity[2] + $Ermine_price * $Quantity[3] + $Fox_price * $Quantity[4]; + print "\nYOU NOW HAVE \$$Money INCLUDING YOUR PREVIOUS SAVINGS\n"; + print "\nDO YOU WANT TO TRADE FURS NEXT YEAR? "; +} +exit(0); + +############################################################### + +sub init +{ + print "YOU ARE THE LEADER OF A FRENCH FUR TRADING EXPEDITION IN\n"; + print "1776 LEAVING THE LAKE ONTARIO AREA TO SELL FURS AND GET\n"; + print "SUPPLIES FOR THE NEXT YEAR. YOU HAVE A CHOICE OF THREE\n"; + print "FORTS AT WHICH YOU MAY TRADE. THE COST OF SUPPLIES\n"; + print "AND THE AMOUNT YOU RECEIVE FOR YOUR FURS WILL DEPEND\n"; + print "ON THE FORT THAT YOU CHOOSE.\n"; + + $Money = 600; + print "DO YOU WISH TO TRADE FURS?\n"; +} + +sub new_price +{ + my ($base, $factor) = @_; + return int(($base * rand(1) + $factor) * 100 + .5) / 100; +} + +sub supplies_fs +{ + print "SUPPLIES AT FORT STADACONA COST \$125.00.\n"; + print "YOUR TRAVEL EXPENSES TO STADACONA WERE \$15.00.\n"; +} + +sub supplies_ny +{ + print "SUPPLIES AT NEW YORK COST \$80.00.\n"; + print "YOUR TRAVEL EXPENSES TO NEW YORK WERE \$25.00.\n"; +} + +sub reset_furs +{ + for my $j (1 .. $Num_pelts) { $Quantity[$j] = 0; } +} + +sub get_yesno +{ + my $ans; + print "ANSWER YES OR NO: "; + chomp($ans = uc(<>)); + return $ans; +} + +sub trade_elsewhere +{ + print "DO YOU WANT TO TRADE AT ANOTHER FORT? "; + my $ans = get_yesno(); + return $ans; +} + +sub fort1 +{ + print "\nYOU HAVE CHOSEN THE EASIEST ROUTE. HOWEVER, THE FORT\n"; + print "IS FAR FROM ANY SEAPORT. THE VALUE\n"; + print "YOU RECEIVE FOR YOUR FURS WILL BE LOW AND THE COST\n"; + print "OF SUPPLIES HIGHER THAN AT FORTS STADACONA OR NEW YORK.\n"; + my $ans = trade_elsewhere(); + if ($ans eq "YES") { return 0; } + + $Money -= 160; + $Mink_price = new_price(.2, .7 ); + $Ermine_price = new_price(.2, .65); + $Beaver_price = new_price(.2, .75); + $Fox_price = new_price(.2, .8 ); + print "\nSUPPLIES AT FORT HOCHELAGA COST \$150.00.\n"; + print "YOUR TRAVEL EXPENSES TO HOCHELAGA WERE \$10.00.\n"; + return 1410; +} + +sub fort2 +{ + print "\nYOU HAVE CHOSEN A HARD ROUTE. IT IS, IN COMPARSION,\n"; + print "HARDER THAN THE ROUTE TO HOCHELAGA BUT EASIER THAN\n"; + print "THE ROUTE TO NEW YORK. YOU WILL RECEIVE AN AVERAGE VALUE\n"; + print "FOR YOUR FURS AND THE COST OF YOUR SUPPLIES WILL BE AVERAGE.\n"; + my $ans = trade_elsewhere(); + if ($ans eq "YES") { return 0; } + + $Money -= 140; + print "\n"; + $Mink_price = new_price(.3, .85); + $Ermine_price = new_price(.15, .8); + $Beaver_price = new_price(.2, .9); + my $P = int(10 * rand(1)) + 1; + if ($P <= 2) + { + $Quantity[2] = 0; + print "YOUR BEAVER WERE TOO HEAVY TO CARRY ACROSS\n"; + print "THE PORTAGE. YOU HAD TO LEAVE THE PELTS, BUT FOUND\n"; + print "THEM STOLEN WHEN YOU RETURNED.\n"; + supplies_fs(); + return 1414; + } + elsif ($P <= 6) + { + print "YOU ARRIVED SAFELY AT FORT STADACONA.\n"; + supplies_fs(); + } + elsif ($P <= 8) + { + reset_furs(); + print "YOUR CANOE UPSET IN THE LACHINE RAPIDS. YOU\n"; + print "LOST ALL YOUR FURS.\n"; + supplies_fs(); + return 1418; + } + elsif ($P <= 10) + { + $Quantity[4] = 0; + print "YOUR FOX PELTS WERE NOT CURED PROPERLY.\n"; + print "NO ONE WILL BUY THEM.\n"; + supplies_fs(); + } + return 1410; +} + +sub fort3 +{ + print "\nYOU HAVE CHOSEN THE MOST DIFFICULT ROUTE. AT\n"; + print "FORT NEW YORK YOU WILL RECEIVE THE HIGHEST VALUE\n"; + print "FOR YOUR FURS. THE COST OF YOUR SUPPLIES\n"; + print "WILL BE LOWER THAN AT ALL THE OTHER FORTS.\n"; + my $ans = trade_elsewhere(); + if ($ans eq "YES") { return 0; } + + $Money -= 105; + print "\n"; + $Mink_price = new_price(.15, 1.05); + $Fox_price = new_price(.25, 1.1); + $Fox_price = new_price(.25, 1.1); + my $P = int(10 * rand(1)) + 1; + if ($P <= 2) + { + print "YOU WERE ATTACKED BY A PARTY OF IROQUOIS.\n"; + print "ALL PEOPLE IN YOUR TRADING GROUP WERE\n"; + print "KILLED. THIS ENDS THE GAME.\n"; + exit(0); + } + elsif ($P <= 6) + { + print "YOU WERE LUCKY. YOU ARRIVED SAFELY\n"; + print "AT FORT NEW YORK.\n"; + supplies_ny(); + } + elsif ($P <= 8) + { + reset_furs(); + print "YOU NARROWLY ESCAPED AN IROQUOIS RAIDING PARTY.\n"; + print "HOWEVER, YOU HAD TO LEAVE ALL YOUR FURS BEHIND.\n"; + supplies_ny(); + return 1418; + } + elsif ($P <= 10) + { + $Beaver_price /= 2; + $Mink_price /= 2; + print "YOUR MINK AND BEAVER WERE DAMAGED ON YOUR TRIP.\n"; + print "YOU RECEIVE ONLY HALF THE CURRENT PRICE FOR THESE FURS.\n"; + supplies_ny(); + } + return 1410; +} diff --git a/42_Gunner/perl/gunner.pl b/42_Gunner/perl/gunner.pl new file mode 100755 index 00000000..9df20deb --- /dev/null +++ b/42_Gunner/perl/gunner.pl @@ -0,0 +1,81 @@ +#!/usr/bin/perl + +# Gunner program in Perl +# Required extensive restructuring to remove all of the GOTO's. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $Max_range = int(40000*rand(1)+20000); +my $Total_shots = 0; +my $Games = 0; + +print "\n"; +print " " x 30, "GUNNER\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +print "YOU ARE THE OFFICER-IN-CHARGE, GIVING ORDERS TO A GUN\n"; +print "CREW, TELLING THEM THE DEGREES OF ELEVATION YOU ESTIMATE\n"; +print "WILL PLACE A PROJECTILE ON TARGET. A HIT WITHIN 100 YARDS\n"; +print "OF THE TARGET WILL DESTROY IT.\n\n"; +print "MAXIMUM RANGE OF YOUR GUN IS $Max_range YARDS.\n\n"; + +GAME: while (1) +{ + my $target_dist = int($Max_range * (.1 + .8 * rand(1))); + my $shots = 0; + print "DISTANCE TO THE TARGET IS $target_dist YARDS.\n\n"; + while (1) + { + my $elevation = get_elevation(); # in degrees + $shots++; + my $dist = int($target_dist - ($Max_range * sin(2 * $elevation / 57.3))); + if (abs($dist) < 100) + { + print "*** TARGET DESTROYED *** $shots ROUNDS OF AMMUNITION EXPENDED.\n"; + $Total_shots += $shots; + if ($Games++ == 4) + { + print "\n\nTOTAL ROUNDS EXPENDED WERE: $Total_shots\n"; + if ($Total_shots > 18) { print "BETTER GO BACK TO FORT SILL FOR REFRESHER TRAINING!\n"; } + else { print "NICE SHOOTING !!\n"; } + last; + } + print "\nTHE FORWARD OBSERVER HAS SIGHTED MORE ENEMY ACTIVITY...\n"; + next GAME; + } + if ($dist > 100) { print "SHORT OF TARGET BY ", abs($dist)," YARDS.\n"; } + else { print "OVER TARGET BY ", abs($dist), " YARDS.\n"; } + + if ($shots >= 5) + { + print "\nBOOM !!!! YOU HAVE JUST BEEN DESTROYED BY THE ENEMY.\n\n\n\n"; + print "BETTER GO BACK TO FORT SILL FOR REFRESHER TRAINING!\n"; + last; + } + } + + print "\nTRY AGAIN (Y OR N): "; + chomp(my $ans=uc(<>)); + if ($ans ne "Y") { last; } + else { $Games = 0; $Total_shots = 0; } +} + +print "\nOK. RETURN TO BASE CAMP.\n"; + +#################################### + +sub get_elevation +{ + my $elevation; + while (1) + { + print "\nELEVATION: "; + chomp($elevation = <>); + if ($elevation > 89) { print "MAXIMUM ELEVATION IS 89 DEGREES.\n"; } + elsif ($elevation < 1) { print "MINIMUM ELEVATION IS ONE DEGREE.\n"; } + else { return $elevation; } + } +} diff --git a/53_King/README.md b/53_King/README.md index a363a2be..14a66fe9 100644 --- a/53_King/README.md +++ b/53_King/README.md @@ -1,10 +1,10 @@ ## King -This is one of the most comprehensive, difficult, and interesting games. (If you’ve never played one of these games, start with HAMMURABI.) +This is one of the most comprehensive, difficult, and interesting games. (If you've never played one of these games, start with HAMMURABI.) In this game, you are Premier of Setats Detinu, a small communist island 30 by 70 miles long. Your job is to decide upon the budget of your country and distribute money to your country from the communal treasury. -The money system is Rollods; each person needs 100 Rallods per year to survive. Your country’s income comes from farm produce and tourists visiting your magnificent forests, hunting, fishing, etc. Part of your land is farm land but it also has an excellent mineral content and may be sold to foreign industry for strip mining. Industry import and support their own workers. Crops cost between 10 and 15 Rallods per square mile to plant, cultivate, and harvest. Your goal is to complete an eight-year term of office without major mishap. A word of warning: it isn’t easy! +The money system is Rollods; each person needs 100 Rallods per year to survive. Your country's income comes from farm produce and tourists visiting your magnificent forests, hunting, fishing, etc. Part of your land is farm land but it also has an excellent mineral content and may be sold to foreign industry for strip mining. Industry import and support their own workers. Crops cost between 10 and 15 Rallods per square mile to plant, cultivate, and harvest. Your goal is to complete an eight-year term of office without major mishap. A word of warning: it isn't easy! The author of this program is James A. Storer who wrote it while a student at Lexington High School. @@ -66,3 +66,15 @@ On basic line 1997 it is: but it should be: 1997 PRINT " AND 1,000 SQ. MILES OF FOREST LAND." + +### Bug 4 + +On basic line 1310 we see this: + + 1310 IF C=0 THEN 1324 + 1320 PRINT "OF ";INT(J);"SQ. MILES PLANTED,"; + 1324 ... + +but it should probably be: + + 1310 IF J=0 THEN 1324 diff --git a/53_King/csharp/Country.cs b/53_King/csharp/Country.cs new file mode 100644 index 00000000..1a37bcd8 --- /dev/null +++ b/53_King/csharp/Country.cs @@ -0,0 +1,133 @@ +namespace King; + +internal class Country +{ + private const int InitialLand = 1000; + + private readonly IReadWrite _io; + private readonly IRandom _random; + private float _rallods; + private float _countrymen; + private float _foreigners; + private float _arableLand; + private float _industryLand; + + public Country(IReadWrite io, IRandom random) + : this( + io, + random, + (int)(60000 + random.NextFloat(1000) - random.NextFloat(1000)), + (int)(500 + random.NextFloat(10) - random.NextFloat(10)), + 0, + InitialLand) + { + } + + public Country(IReadWrite io, IRandom random, float rallods, float countrymen, float foreigners, float land) + { + _io = io; + _random = random; + _rallods = rallods; + _countrymen = countrymen; + _foreigners = foreigners; + _arableLand = land; + } + + public string GetStatus(int landValue, int plantingCost) + => Resource.Status(_rallods, _countrymen, _foreigners, _arableLand, landValue, plantingCost); + + public float Countrymen => _countrymen; + public float Workers => _foreigners; + public bool HasWorkers => _foreigners > 0; + private float FarmLand => _arableLand; + public bool HasRallods => _rallods > 0; + public float Rallods => _rallods; + public float IndustryLand => InitialLand - _arableLand; + public int PreviousTourismIncome { get; private set; } + + public bool SellLand(int landValue, out float landSold) + { + if (_io.TryReadValue( + SellLandPrompt, + out landSold, + new ValidityTest(v => v <= FarmLand, () => SellLandError(FarmLand)))) + { + _arableLand = (int)(_arableLand - landSold); + _rallods = (int)(_rallods + landSold * landValue); + return true; + } + + return false; + } + + public bool DistributeRallods(out float rallodsGiven) + { + if (_io.TryReadValue( + GiveRallodsPrompt, + out rallodsGiven, + new ValidityTest(v => v <= _rallods, () => GiveRallodsError(_rallods)))) + { + _rallods = (int)(_rallods - rallodsGiven); + return true; + } + + return false; + } + + public bool PlantLand(int plantingCost, out float landPlanted) + { + if (_io.TryReadValue( + PlantLandPrompt, + out landPlanted, + new ValidityTest(v => v <= _countrymen * 2, PlantLandError1), + new ValidityTest(v => v <= FarmLand, PlantLandError2(FarmLand)), + new ValidityTest(v => v * plantingCost <= _rallods, PlantLandError3(_rallods)))) + { + _rallods -= (int)(landPlanted * plantingCost); + return true; + } + + return false; + } + + public bool ControlPollution(out float rallodsSpent) + { + if (_io.TryReadValue( + PollutionPrompt, + out rallodsSpent, + new ValidityTest(v => v <= _rallods, () => PollutionError(_rallods)))) + { + _rallods = (int)(_rallods - rallodsSpent); + return true; + } + + return false; + } + + public bool TrySpend(float amount, float landValue) + { + if (_rallods >= amount) + { + _rallods -= amount; + return true; + } + + _arableLand = (int)(_arableLand - (int)(amount - _rallods) / landValue); + _rallods = 0; + return false; + } + + public void RemoveTheDead(int deaths) => _countrymen = (int)(_countrymen - deaths); + + public void Migration(int migration) => _countrymen = (int)(_countrymen + migration); + + public void AddWorkers(int newWorkers) => _foreigners = (int)(_foreigners + newWorkers); + + public void SellCrops(int income) => _rallods = (int)(_rallods + income); + + public void EntertainTourists(int income) + { + PreviousTourismIncome = income; + _rallods = (int)(_rallods + income); + } +} diff --git a/53_King/csharp/Game.cs b/53_King/csharp/Game.cs new file mode 100644 index 00000000..6a91010c --- /dev/null +++ b/53_King/csharp/Game.cs @@ -0,0 +1,47 @@ +namespace King; + +internal class Game +{ + const int TermOfOffice = 8; + + private readonly IReadWrite _io; + private readonly IRandom _random; + + public Game(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + } + + public void Play() + { + _io.Write(Title); + + var reign = SetUpReign(); + if (reign != null) + { + while (reign.PlayYear()); + } + + _io.WriteLine(); + _io.WriteLine(); + } + + private Reign? SetUpReign() + { + var response = _io.ReadString(InstructionsPrompt).ToUpper(); + + if (response.Equals("Again", StringComparison.InvariantCultureIgnoreCase)) + { + return _io.TryReadGameData(_random, out var reign) ? reign : null; + } + + if (!response.StartsWith("N", StringComparison.InvariantCultureIgnoreCase)) + { + _io.Write(InstructionsText(TermOfOffice)); + } + + _io.WriteLine(); + return new Reign(_io, _random); + } +} diff --git a/53_King/csharp/IOExtensions.cs b/53_King/csharp/IOExtensions.cs new file mode 100644 index 00000000..dbb3079a --- /dev/null +++ b/53_King/csharp/IOExtensions.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; +using static King.Resources.Resource; + +namespace King; + +internal static class IOExtensions +{ + internal static bool TryReadGameData(this IReadWrite io, IRandom random, [NotNullWhen(true)] out Reign? reign) + { + if (io.TryReadValue(SavedYearsPrompt, v => v < Reign.MaxTerm, SavedYearsError(Reign.MaxTerm), out var years) && + io.TryReadValue(SavedTreasuryPrompt, out var rallods) && + io.TryReadValue(SavedCountrymenPrompt, out var countrymen) && + io.TryReadValue(SavedWorkersPrompt, out var workers) && + io.TryReadValue(SavedLandPrompt, v => v is > 1000 and <= 2000, SavedLandError, out var land)) + { + reign = new Reign(io, random, new Country(io, random, rallods, countrymen, workers, land), years + 1); + return true; + } + + reign = default; + return false; + } + + internal static bool TryReadValue(this IReadWrite io, string prompt, out float value, params ValidityTest[] tests) + { + while (true) + { + var response = value = io.ReadNumber(prompt); + if (response == 0) { return false; } + if (tests.All(test => test.IsValid(response, io))) { return true; } + } + } + + internal static bool TryReadValue(this IReadWrite io, string prompt, out float value) + => io.TryReadValue(prompt, _ => true, "", out value); + + internal static bool TryReadValue( + this IReadWrite io, + string prompt, + Predicate isValid, + string error, + out float value) + => io.TryReadValue(prompt, isValid, () => error, out value); + + internal static bool TryReadValue( + this IReadWrite io, + string prompt, + Predicate isValid, + Func getError, + out float value) + { + while (true) + { + value = io.ReadNumber(prompt); + if (value < 0) { return false; } + if (isValid(value)) { return true; } + + io.Write(getError()); + } + } +} diff --git a/53_King/csharp/King.csproj b/53_King/csharp/King.csproj index d3fe4757..3870320c 100644 --- a/53_King/csharp/King.csproj +++ b/53_King/csharp/King.csproj @@ -6,4 +6,12 @@ enable enable + + + + + + + + diff --git a/53_King/csharp/Program.cs b/53_King/csharp/Program.cs new file mode 100644 index 00000000..5aae4ccb --- /dev/null +++ b/53_King/csharp/Program.cs @@ -0,0 +1,7 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; +global using King.Resources; +global using static King.Resources.Resource; +using King; + +new Game(new ConsoleIO(), new RandomNumberGenerator()).Play(); diff --git a/53_King/csharp/Reign.cs b/53_King/csharp/Reign.cs new file mode 100644 index 00000000..7a96a30f --- /dev/null +++ b/53_King/csharp/Reign.cs @@ -0,0 +1,45 @@ +namespace King; + +internal class Reign +{ + public const int MaxTerm = 8; + + private readonly IReadWrite _io; + private readonly IRandom _random; + private readonly Country _country; + private float _yearNumber; + + public Reign(IReadWrite io, IRandom random) + : this(io, random, new Country(io, random), 1) + { + } + + public Reign(IReadWrite io, IRandom random, Country country, float year) + { + _io = io; + _random = random; + _country = country; + _yearNumber = year; + } + + public bool PlayYear() + { + var year = new Year(_country, _random, _io); + + _io.Write(year.Status); + + var result = year.GetPlayerActions() ?? year.EvaluateResults() ?? IsAtEndOfTerm(); + if (result.IsGameOver) + { + _io.WriteLine(result.Message); + return false; + } + + return true; + } + + private Result IsAtEndOfTerm() + => _yearNumber == MaxTerm + ? Result.GameOver(EndCongratulations(MaxTerm)) + : Result.Continue; +} diff --git a/53_King/csharp/Resources/DeathsPollution.txt b/53_King/csharp/Resources/DeathsPollution.txt new file mode 100644 index 00000000..bf28d9a7 --- /dev/null +++ b/53_King/csharp/Resources/DeathsPollution.txt @@ -0,0 +1 @@ + {0} countrymen died of carbon-monoxide and dust inhalation \ No newline at end of file diff --git a/53_King/csharp/Resources/DeathsStarvation.txt b/53_King/csharp/Resources/DeathsStarvation.txt new file mode 100644 index 00000000..af275cf4 --- /dev/null +++ b/53_King/csharp/Resources/DeathsStarvation.txt @@ -0,0 +1 @@ + {0} countrymen died of starvation \ No newline at end of file diff --git a/53_King/csharp/Resources/Emigration.txt b/53_King/csharp/Resources/Emigration.txt new file mode 100644 index 00000000..01f67094 --- /dev/null +++ b/53_King/csharp/Resources/Emigration.txt @@ -0,0 +1 @@ + {0} countrymen left the island. \ No newline at end of file diff --git a/53_King/csharp/Resources/EndAlso.txt b/53_King/csharp/Resources/EndAlso.txt new file mode 100644 index 00000000..084b9a82 --- /dev/null +++ b/53_King/csharp/Resources/EndAlso.txt @@ -0,0 +1,3 @@ +also had your left eye gouged out! +;have also gained a very bad reputation. +;have also been declared national fink. diff --git a/53_King/csharp/Resources/EndCongratulations.txt b/53_King/csharp/Resources/EndCongratulations.txt new file mode 100644 index 00000000..ef1ff203 --- /dev/null +++ b/53_King/csharp/Resources/EndCongratulations.txt @@ -0,0 +1,10 @@ + + +Congratulations!!!!!!!!!!!!!!!!!! +You have successfully completed your {0} year term +of office. You were, of course, extremely lucky, but +nevertheless, it's quite an achievement. Goodbye and good +luck - you'll probably need it if you're the type that +plays this game. + + diff --git a/53_King/csharp/Resources/EndConsequences.txt b/53_King/csharp/Resources/EndConsequences.txt new file mode 100644 index 00000000..61ebfd25 --- /dev/null +++ b/53_King/csharp/Resources/EndConsequences.txt @@ -0,0 +1,3 @@ +You have been thrown out of office and are now +residing in prison.; +You have been assassinated. diff --git a/53_King/csharp/Resources/EndForeignWorkers.txt b/53_King/csharp/Resources/EndForeignWorkers.txt new file mode 100644 index 00000000..76c04c3d --- /dev/null +++ b/53_King/csharp/Resources/EndForeignWorkers.txt @@ -0,0 +1,7 @@ + + +The number of foreign workers has exceeded the number +of countrymen. As a minority, they have revolted and +taken over the country. +{0} + diff --git a/53_King/csharp/Resources/EndManyDead.txt b/53_King/csharp/Resources/EndManyDead.txt new file mode 100644 index 00000000..70cd95f0 --- /dev/null +++ b/53_King/csharp/Resources/EndManyDead.txt @@ -0,0 +1,5 @@ +{0} countrymen died in one year!!!!! +due to this extreme mismanagement, you have not only +been impeached and thrown out of office, but you +{1} + diff --git a/53_King/csharp/Resources/EndMoneyLeftOver.txt b/53_King/csharp/Resources/EndMoneyLeftOver.txt new file mode 100644 index 00000000..e194a9fd --- /dev/null +++ b/53_King/csharp/Resources/EndMoneyLeftOver.txt @@ -0,0 +1,7 @@ + +Money was left over in the treasury which you did +not spend. As a result, some of your countrymen died +of starvation. The public is enraged and you have +been forced to resign. + + diff --git a/53_King/csharp/Resources/EndOneThirdDead.txt b/53_King/csharp/Resources/EndOneThirdDead.txt new file mode 100644 index 00000000..e1761dfe --- /dev/null +++ b/53_King/csharp/Resources/EndOneThirdDead.txt @@ -0,0 +1,7 @@ + + +Over one third of the population has died since you +were elected to office. The people (remaining) +hate your guts. +{0} + diff --git a/53_King/csharp/Resources/FuneralExpenses.txt b/53_King/csharp/Resources/FuneralExpenses.txt new file mode 100644 index 00000000..3aff58eb --- /dev/null +++ b/53_King/csharp/Resources/FuneralExpenses.txt @@ -0,0 +1 @@ + You were forced to spend {0} rallods on funeral expenses \ No newline at end of file diff --git a/53_King/csharp/Resources/GiveRallodsError.txt b/53_King/csharp/Resources/GiveRallodsError.txt new file mode 100644 index 00000000..df0d7cc0 --- /dev/null +++ b/53_King/csharp/Resources/GiveRallodsError.txt @@ -0,0 +1 @@ + Think again. You've only got {0} rallods in the treasury. diff --git a/53_King/csharp/Resources/GiveRallodsPrompt.txt b/53_King/csharp/Resources/GiveRallodsPrompt.txt new file mode 100644 index 00000000..e8bd345b --- /dev/null +++ b/53_King/csharp/Resources/GiveRallodsPrompt.txt @@ -0,0 +1 @@ +How many rallods will you distribute among your countrymen \ No newline at end of file diff --git a/53_King/csharp/Resources/Goodbye.txt b/53_King/csharp/Resources/Goodbye.txt new file mode 100644 index 00000000..67543c04 --- /dev/null +++ b/53_King/csharp/Resources/Goodbye.txt @@ -0,0 +1,4 @@ +Goodbye. +(If you wish to continue this game at a later date, answer +'again' when asked if you want instructions at the start +of the game). diff --git a/53_King/csharp/Resources/Harvest.txt b/53_King/csharp/Resources/Harvest.txt new file mode 100644 index 00000000..287e0c42 --- /dev/null +++ b/53_King/csharp/Resources/Harvest.txt @@ -0,0 +1,2 @@ + you harvested {0} sq. miles of crops. +{1}making {2} rallods. diff --git a/53_King/csharp/Resources/HarvestReason.txt b/53_King/csharp/Resources/HarvestReason.txt new file mode 100644 index 00000000..82faccb5 --- /dev/null +++ b/53_King/csharp/Resources/HarvestReason.txt @@ -0,0 +1 @@ + (Due to increased air and water pollution from foreign industry.) diff --git a/53_King/csharp/Resources/Immigration.txt b/53_King/csharp/Resources/Immigration.txt new file mode 100644 index 00000000..30d8b6b3 --- /dev/null +++ b/53_King/csharp/Resources/Immigration.txt @@ -0,0 +1 @@ + {0} countrymen came to the island. \ No newline at end of file diff --git a/53_King/csharp/Resources/InstructionsPrompt.txt b/53_King/csharp/Resources/InstructionsPrompt.txt new file mode 100644 index 00000000..0d311b60 --- /dev/null +++ b/53_King/csharp/Resources/InstructionsPrompt.txt @@ -0,0 +1 @@ +Do you want instructions \ No newline at end of file diff --git a/53_King/csharp/Resources/InstructionsText.txt b/53_King/csharp/Resources/InstructionsText.txt new file mode 100644 index 00000000..c576123b --- /dev/null +++ b/53_King/csharp/Resources/InstructionsText.txt @@ -0,0 +1,17 @@ + + + +Congratulations! You've just been elected Premier of Setats +Detinu, a small communist island 30 by 70 miles long. Your +job is to decide upon the country's budget and distribute +money to your countrymen from the communal treasury. +The money system is rallods, and each person needs 100 +rallods per year to survive. Your country's income comes +from farm produce and tourists visiting your magnificent +forests, hunting, fishing, etc. Half your land if farm land +which also has an excellent mineral content and may be sold +to foreign industry (strip mining) who import and support +their own workers. Crops cost between 10 and 15 rallods per +square mile to plant. +Your goal is to complete your {0} year term of office. +Good luck! diff --git a/53_King/csharp/Resources/InsufficientReserves.txt b/53_King/csharp/Resources/InsufficientReserves.txt new file mode 100644 index 00000000..e69de29b diff --git a/53_King/csharp/Resources/LandPlanted.txt b/53_King/csharp/Resources/LandPlanted.txt new file mode 100644 index 00000000..e52529aa --- /dev/null +++ b/53_King/csharp/Resources/LandPlanted.txt @@ -0,0 +1 @@ +Of {0} sq. miles planted, \ No newline at end of file diff --git a/53_King/csharp/Resources/PlantLandError1.txt b/53_King/csharp/Resources/PlantLandError1.txt new file mode 100644 index 00000000..ef959f9f --- /dev/null +++ b/53_King/csharp/Resources/PlantLandError1.txt @@ -0,0 +1 @@ + Sorry, but each countryman can only plant 2 sq. miles. \ No newline at end of file diff --git a/53_King/csharp/Resources/PlantLandError2.txt b/53_King/csharp/Resources/PlantLandError2.txt new file mode 100644 index 00000000..e5844aa1 --- /dev/null +++ b/53_King/csharp/Resources/PlantLandError2.txt @@ -0,0 +1 @@ + Sorry, but you've only {0} sq. miles of farm land. \ No newline at end of file diff --git a/53_King/csharp/Resources/PlantLandError3.txt b/53_King/csharp/Resources/PlantLandError3.txt new file mode 100644 index 00000000..38264442 --- /dev/null +++ b/53_King/csharp/Resources/PlantLandError3.txt @@ -0,0 +1 @@ + Think again, You've only {0} rallods left in the treasury. diff --git a/53_King/csharp/Resources/PlantLandPrompt.txt b/53_King/csharp/Resources/PlantLandPrompt.txt new file mode 100644 index 00000000..f2fe448e --- /dev/null +++ b/53_King/csharp/Resources/PlantLandPrompt.txt @@ -0,0 +1 @@ +How many square miles do you wish to plant \ No newline at end of file diff --git a/53_King/csharp/Resources/PollutionError.txt b/53_King/csharp/Resources/PollutionError.txt new file mode 100644 index 00000000..f99644e3 --- /dev/null +++ b/53_King/csharp/Resources/PollutionError.txt @@ -0,0 +1 @@ + Think again. You only have {0} rallods remaining. diff --git a/53_King/csharp/Resources/PollutionPrompt.txt b/53_King/csharp/Resources/PollutionPrompt.txt new file mode 100644 index 00000000..252fb333 --- /dev/null +++ b/53_King/csharp/Resources/PollutionPrompt.txt @@ -0,0 +1 @@ +How many rallods do you wish to spend on pollution control \ No newline at end of file diff --git a/53_King/csharp/Resources/Resource.cs b/53_King/csharp/Resources/Resource.cs new file mode 100644 index 00000000..5ea09e1c --- /dev/null +++ b/53_King/csharp/Resources/Resource.cs @@ -0,0 +1,112 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace King.Resources; + +internal static class Resource +{ + private static bool _sellLandErrorShown; + + public static Stream Title => GetStream(); + + public static string InstructionsPrompt => GetString(); + public static string InstructionsText(int years) => string.Format(GetString(), years); + + public static string Status( + float rallods, + float countrymen, + float workers, + float land, + float landValue, + float plantingCost) + => string.Format( + workers == 0 ? StatusWithWorkers : StatusSansWorkers, + rallods, + (int)countrymen, + (int)workers, + (int)land, + landValue, + plantingCost); + + private static string StatusWithWorkers => GetString(); + private static string StatusSansWorkers => GetString(); + + public static string SellLandPrompt => GetString(); + public static string SellLandError(float farmLand) + { + var error = string.Format(GetString(), farmLand, _sellLandErrorShown ? "" : SellLandErrorReason); + _sellLandErrorShown = true; + return error; + } + private static string SellLandErrorReason => GetString(); + + public static string GiveRallodsPrompt => GetString(); + public static string GiveRallodsError(float rallods) => string.Format(GetString(), rallods); + + public static string PlantLandPrompt => GetString(); + public static string PlantLandError1 => GetString(); + public static string PlantLandError2(float farmLand) => string.Format(GetString(), farmLand); + public static string PlantLandError3(float rallods) => string.Format(GetString(), rallods); + + public static string PollutionPrompt => GetString(); + public static string PollutionError(float rallods) => string.Format(GetString(), rallods); + + public static string DeathsStarvation(float deaths) => string.Format(GetString(), (int)deaths); + public static string DeathsPollution(int deaths) => string.Format(GetString(), deaths); + public static string FuneralExpenses(int expenses) => string.Format(GetString(), expenses); + public static string InsufficientReserves => GetString(); + + public static string WorkerMigration(int newWorkers) => string.Format(GetString(), newWorkers); + public static string Migration(int migration) + => string.Format(migration < 0 ? Emigration : Immigration, Math.Abs(migration)); + public static string Emigration => GetString(); + public static string Immigration => GetString(); + + public static string LandPlanted(float landPlanted) + => landPlanted > 0 ? string.Format(GetString(), (int)landPlanted) : ""; + public static string Harvest(int yield, int income, bool hasIndustry) + => string.Format(GetString(), yield, HarvestReason(hasIndustry), income); + private static string HarvestReason(bool hasIndustry) => hasIndustry ? GetString() : ""; + + public static string TourismEarnings(int income) => string.Format(GetString(), income); + public static string TourismDecrease(IRandom random) => string.Format(GetString(), TourismReason(random)); + private static string TourismReason(IRandom random) => GetStrings()[random.Next(5)]; + + private static string EndAlso(IRandom random) + => random.Next(10) switch + { + <= 3 => GetStrings()[0], + <= 6 => GetStrings()[1], + _ => GetStrings()[2] + }; + + public static string EndCongratulations(int termLength) => string.Format(GetString(), termLength); + private static string EndConsequences(IRandom random) => GetStrings()[random.Next(2)]; + public static string EndForeignWorkers(IRandom random) => string.Format(GetString(), EndConsequences(random)); + public static string EndManyDead(int deaths, IRandom random) => string.Format(GetString(), deaths, EndAlso(random)); + public static string EndMoneyLeftOver() => GetString(); + public static string EndOneThirdDead(IRandom random) => string.Format(GetString(), EndConsequences(random)); + + public static string SavedYearsPrompt => GetString(); + public static string SavedYearsError(int years) => string.Format(GetString(), years); + public static string SavedTreasuryPrompt => GetString(); + public static string SavedCountrymenPrompt => GetString(); + public static string SavedWorkersPrompt => GetString(); + public static string SavedLandPrompt => GetString(); + public static string SavedLandError => GetString(); + + public static string Goodbye => GetString(); + + private static string[] GetStrings([CallerMemberName] string? name = null) => GetString(name).Split(';'); + + private static string GetString([CallerMemberName] string? name = null) + { + using var stream = GetStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/53_King/csharp/Resources/SavedCountrymenPrompt.txt b/53_King/csharp/Resources/SavedCountrymenPrompt.txt new file mode 100644 index 00000000..120718d2 --- /dev/null +++ b/53_King/csharp/Resources/SavedCountrymenPrompt.txt @@ -0,0 +1 @@ +How many countrymen \ No newline at end of file diff --git a/53_King/csharp/Resources/SavedLandError.txt b/53_King/csharp/Resources/SavedLandError.txt new file mode 100644 index 00000000..9ac66dad --- /dev/null +++ b/53_King/csharp/Resources/SavedLandError.txt @@ -0,0 +1,2 @@ + Come on, you started with 1000 sq. miles of farm land + and 10,000 sq. miles of forest land diff --git a/53_King/csharp/Resources/SavedLandPrompt.txt b/53_King/csharp/Resources/SavedLandPrompt.txt new file mode 100644 index 00000000..e8afdf72 --- /dev/null +++ b/53_King/csharp/Resources/SavedLandPrompt.txt @@ -0,0 +1 @@ +How many square miles of land \ No newline at end of file diff --git a/53_King/csharp/Resources/SavedTreasuryPrompt.txt b/53_King/csharp/Resources/SavedTreasuryPrompt.txt new file mode 100644 index 00000000..f4a50f58 --- /dev/null +++ b/53_King/csharp/Resources/SavedTreasuryPrompt.txt @@ -0,0 +1 @@ +How much did you have in the treasury \ No newline at end of file diff --git a/53_King/csharp/Resources/SavedWorkersPrompt.txt b/53_King/csharp/Resources/SavedWorkersPrompt.txt new file mode 100644 index 00000000..b88a9c10 --- /dev/null +++ b/53_King/csharp/Resources/SavedWorkersPrompt.txt @@ -0,0 +1 @@ +How many workers \ No newline at end of file diff --git a/53_King/csharp/Resources/SavedYearsError.txt b/53_King/csharp/Resources/SavedYearsError.txt new file mode 100644 index 00000000..1c8c3c99 --- /dev/null +++ b/53_King/csharp/Resources/SavedYearsError.txt @@ -0,0 +1 @@ + Come on, your term in office is only {0} years. diff --git a/53_King/csharp/Resources/SavedYearsPrompt.txt b/53_King/csharp/Resources/SavedYearsPrompt.txt new file mode 100644 index 00000000..afbda229 --- /dev/null +++ b/53_King/csharp/Resources/SavedYearsPrompt.txt @@ -0,0 +1 @@ +How many years had you been in office when interrupted \ No newline at end of file diff --git a/53_King/csharp/Resources/SellLandError.txt b/53_King/csharp/Resources/SellLandError.txt new file mode 100644 index 00000000..83e0266c --- /dev/null +++ b/53_King/csharp/Resources/SellLandError.txt @@ -0,0 +1,2 @@ +*** Think again. You only have {0} square miles of farm land. +{1} \ No newline at end of file diff --git a/53_King/csharp/Resources/SellLandErrorReason.txt b/53_King/csharp/Resources/SellLandErrorReason.txt new file mode 100644 index 00000000..836352bb --- /dev/null +++ b/53_King/csharp/Resources/SellLandErrorReason.txt @@ -0,0 +1,4 @@ + +(Foreign industry will only buy farm land because +forest land is uneconomical to strip mine due to trees, +thicker top soil, etc.) diff --git a/53_King/csharp/Resources/SellLandPrompt.txt b/53_King/csharp/Resources/SellLandPrompt.txt new file mode 100644 index 00000000..8d01c2c2 --- /dev/null +++ b/53_King/csharp/Resources/SellLandPrompt.txt @@ -0,0 +1 @@ +How many square miles do you wish to sell to industry \ No newline at end of file diff --git a/53_King/csharp/Resources/StatusSansWorkers.txt b/53_King/csharp/Resources/StatusSansWorkers.txt new file mode 100644 index 00000000..7efb84e7 --- /dev/null +++ b/53_King/csharp/Resources/StatusSansWorkers.txt @@ -0,0 +1,6 @@ + +You now have {0} rallods in the treasury. + {1} countrymen, and {3} sq. miles of land. +This year industry will buy land for {4} rallods per square mile. +Land currently costs {5} rallods per square mile to plant. + diff --git a/53_King/csharp/Resources/StatusWithWorkers.txt b/53_King/csharp/Resources/StatusWithWorkers.txt new file mode 100644 index 00000000..e7bc5c4a --- /dev/null +++ b/53_King/csharp/Resources/StatusWithWorkers.txt @@ -0,0 +1,6 @@ + +You now have {0} rallods in the treasury. + {1} countrymen, {2} foreign workers and {3} sq. miles of land. +This year industry will buy land for {4} rallods per square mile. +Land currently costs {5} rallods per square mile to plant. + diff --git a/53_King/csharp/Resources/Title.txt b/53_King/csharp/Resources/Title.txt new file mode 100644 index 00000000..f9f7854d --- /dev/null +++ b/53_King/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + King + Creative Computing Morristown, New Jersey + + + diff --git a/53_King/csharp/Resources/TourismDecrease.txt b/53_King/csharp/Resources/TourismDecrease.txt new file mode 100644 index 00000000..dcb45fc7 --- /dev/null +++ b/53_King/csharp/Resources/TourismDecrease.txt @@ -0,0 +1 @@ + Decrease because {0} \ No newline at end of file diff --git a/53_King/csharp/Resources/TourismEarnings.txt b/53_King/csharp/Resources/TourismEarnings.txt new file mode 100644 index 00000000..db9251dd --- /dev/null +++ b/53_King/csharp/Resources/TourismEarnings.txt @@ -0,0 +1 @@ + You made {0} rallods from tourist trade. \ No newline at end of file diff --git a/53_King/csharp/Resources/TourismReason.txt b/53_King/csharp/Resources/TourismReason.txt new file mode 100644 index 00000000..b3535c3d --- /dev/null +++ b/53_King/csharp/Resources/TourismReason.txt @@ -0,0 +1,5 @@ +fish population has dwindled due to water pollution. +;air pollution is killing game bird population. +;mineral baths are being ruined by water pollution. +;unpleasant smog is discouraging sun bathers. +;hotels are looking shabby due to smog grit. diff --git a/53_King/csharp/Resources/WorkerMigration.txt b/53_King/csharp/Resources/WorkerMigration.txt new file mode 100644 index 00000000..d2f3b378 --- /dev/null +++ b/53_King/csharp/Resources/WorkerMigration.txt @@ -0,0 +1 @@ + {0} workers came to the country and \ No newline at end of file diff --git a/53_King/csharp/Result.cs b/53_King/csharp/Result.cs new file mode 100644 index 00000000..185f4617 --- /dev/null +++ b/53_King/csharp/Result.cs @@ -0,0 +1,7 @@ +namespace King; + +internal record struct Result (bool IsGameOver, string Message) +{ + internal static Result GameOver(string message) => new(true, message); + internal static Result Continue => new(false, ""); +} diff --git a/53_King/csharp/ValidityTest.cs b/53_King/csharp/ValidityTest.cs new file mode 100644 index 00000000..f1aeb5ec --- /dev/null +++ b/53_King/csharp/ValidityTest.cs @@ -0,0 +1,26 @@ +namespace King; + +internal class ValidityTest +{ + private readonly Predicate _isValid; + private readonly Func _getError; + + public ValidityTest(Predicate isValid, string error) + : this(isValid, () => error) + { + } + + public ValidityTest(Predicate isValid, Func getError) + { + _isValid = isValid; + _getError = getError; + } + + public bool IsValid(float value, IReadWrite io) + { + if (_isValid(value)) { return true; } + + io.Write(_getError()); + return false; + } +} \ No newline at end of file diff --git a/53_King/csharp/Year.cs b/53_King/csharp/Year.cs new file mode 100644 index 00000000..8e6bb0b4 --- /dev/null +++ b/53_King/csharp/Year.cs @@ -0,0 +1,154 @@ +using System.Text; + +namespace King; + +internal class Year +{ + private readonly Country _country; + private readonly IRandom _random; + private readonly IReadWrite _io; + private readonly int _plantingCost; + private readonly int _landValue; + + private float _landSold; + private float _rallodsDistributed; + private float _landPlanted; + private float _pollutionControlCost; + + private float _citizenSupport; + private int _deaths; + private float _starvationDeaths; + private int _pollutionDeaths; + private int _migration; + + public Year(Country country, IRandom random, IReadWrite io) + { + _country = country; + _random = random; + _io = io; + + _plantingCost = random.Next(10, 15); + _landValue = random.Next(95, 105); + } + + public string Status => _country.GetStatus(_landValue, _plantingCost); + + public Result? GetPlayerActions() + { + var playerSoldLand = _country.SellLand(_landValue, out _landSold); + var playerDistributedRallods = _country.DistributeRallods(out _rallodsDistributed); + var playerPlantedLand = _country.HasRallods && _country.PlantLand(_plantingCost, out _landPlanted); + var playerControlledPollution = _country.HasRallods && _country.ControlPollution(out _pollutionControlCost); + + return playerSoldLand || playerDistributedRallods || playerPlantedLand || playerControlledPollution + ? null + : Result.GameOver(Goodbye); + } + + public Result? EvaluateResults() + { + var rallodsUnspent = _country.Rallods; + + _io.WriteLine(); + _io.WriteLine(); + + return EvaluateDeaths() + ?? EvaluateMigration() + ?? EvaluateAgriculture() + ?? EvaluateTourism() + ?? DetermineResult(rallodsUnspent); + } + + public Result? EvaluateDeaths() + { + var supportedCountrymen = _rallodsDistributed / 100; + _citizenSupport = supportedCountrymen - _country.Countrymen; + _starvationDeaths = -_citizenSupport; + if (_starvationDeaths > 0) + { + if (supportedCountrymen < 50) { return Result.GameOver(EndOneThirdDead(_random)); } + _io.WriteLine(DeathsStarvation(_starvationDeaths)); + } + + var pollutionControl = _pollutionControlCost >= 25 ? _pollutionControlCost / 25 : 1; + _pollutionDeaths = (int)(_random.Next((int)_country.IndustryLand) / pollutionControl); + if (_pollutionDeaths > 0) + { + _io.WriteLine(DeathsPollution(_pollutionDeaths)); + } + + _deaths = (int)(_starvationDeaths + _pollutionDeaths); + if (_deaths > 0) + { + var funeralCosts = _deaths * 9; + _io.WriteLine(FuneralExpenses(funeralCosts)); + + if (!_country.TrySpend(funeralCosts, _landValue)) + { + _io.WriteLine(InsufficientReserves); + } + + _country.RemoveTheDead(_deaths); + } + + return null; + } + + private Result? EvaluateMigration() + { + if (_landSold > 0) + { + var newWorkers = (int)(_landSold + _random.NextFloat(10) - _random.NextFloat(20)); + if (!_country.HasWorkers) { newWorkers += 20; } + _io.Write(WorkerMigration(newWorkers)); + _country.AddWorkers(newWorkers); + } + + _migration = + (int)(_citizenSupport / 10 + _pollutionControlCost / 25 - _country.IndustryLand / 50 - _pollutionDeaths / 2); + _io.WriteLine(Migration(_migration)); + _country.Migration(_migration); + + return null; + } + + private Result? EvaluateAgriculture() + { + var ruinedCrops = (int)Math.Min(_country.IndustryLand * (_random.NextFloat() + 1.5f) / 2, _landPlanted); + var yield = (int)(_landPlanted - ruinedCrops); + var income = (int)(yield * _landValue / 2f); + + _io.Write(LandPlanted(_landPlanted)); + _io.Write(Harvest(yield, income, _country.IndustryLand > 0)); + + _country.SellCrops(income); + + return null; + } + + private Result? EvaluateTourism() + { + var reputationValue = (int)((_country.Countrymen - _migration) * 22 + _random.NextFloat(500)); + var industryAdjustment = (int)(_country.IndustryLand * 15); + var tourismIncome = Math.Abs(reputationValue - industryAdjustment); + + _io.WriteLine(TourismEarnings(tourismIncome)); + if (industryAdjustment > 0 && tourismIncome < _country.PreviousTourismIncome) + { + _io.Write(TourismDecrease(_random)); + } + + _country.EntertainTourists(tourismIncome); + + return null; + } + + private Result? DetermineResult(float rallodsUnspent) + { + if (_deaths > 200) { return Result.GameOver(EndManyDead(_deaths, _random)); } + if (_country.Countrymen < 343) { return Result.GameOver(EndOneThirdDead(_random)); } + if (rallodsUnspent / 100 > 5 && _starvationDeaths >= 2) { return Result.GameOver(EndMoneyLeftOver()); } + if (_country.Workers > _country.Countrymen) { return Result.GameOver(EndForeignWorkers(_random)); } + return null; + } +} diff --git a/56_Life_for_Two/README.md b/56_Life_for_Two/README.md index 3d906342..e68c8d64 100644 --- a/56_Life_for_Two/README.md +++ b/56_Life_for_Two/README.md @@ -48,3 +48,17 @@ http://www.vintage-basic.net/games.html #### Porting Notes (please note any difficulties or challenges in porting here) + +Note: The original program has a bug. The instructions say that if both players +enter the same cell that the cell is set to 0 or empty. However, the original +Basic program tells the player "ILLEGAL COORDINATES" and makes another cell be entered, +giving a slightly unfair advantage to the 2nd player. + +The Perl verson of the program fixes the bug and follows the instructions. + +Note: The original code had "GOTO 800" but label 800 didn't exist; it should have gone to label 999. +The Basic program has been fixed. + +Note: The Basic program is written to assume it's being played on a Teletype, i.e. output is printed +on paper. To play on a terminal the input must not be echoed, which can be a challenge to do portably +and without tying the solution to a specific OS. Some versions may tell you how to do this, others might not. diff --git a/56_Life_for_Two/lifefortwo.bas b/56_Life_for_Two/lifefortwo.bas index e970ef6d..4faf1f12 100644 --- a/56_Life_for_Two/lifefortwo.bas +++ b/56_Life_for_Two/lifefortwo.bas @@ -60,8 +60,8 @@ 571 IF M3=0 THEN B=1: GOTO 575 572 IF M2=0 THEN B=2: GOTO 575 573 GOTO 580 -574 PRINT: PRINT "A DRAW":GOTO 800 -575 PRINT: PRINT "PLAYER";B;"IS THE WINNER":GOTO 800 +574 PRINT: PRINT "A DRAW":GOTO 999 +575 PRINT: PRINT "PLAYER";B;"IS THE WINNER":GOTO 999 580 FOR B=1 TO 2: PRINT: PRINT: PRINT "PLAYER";B;: GOSUB 700 581 IF B=99 THEN 560 582 NEXT B diff --git a/56_Life_for_Two/perl/README.md b/56_Life_for_Two/perl/README.md index e69c8b81..7b3b41a3 100644 --- a/56_Life_for_Two/perl/README.md +++ b/56_Life_for_Two/perl/README.md @@ -1,3 +1,21 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +Note: The original program has a bug (see the README in the above dir). This Perl version fixes it. + +Note: For input, the X value is to the right while the Y value is down. +Therefore, the top right cell is "5,1", not "1,5". + +The original program was made to be played on a Teletype, i.e. a printer on paper. +That allowed the program to "black out" the input line to hide a user's input from his/her +opponent, assuming the opponent was at least looking away. To do the equivalent on a +terminal would require a Perl module that isn't installed by default (i.e. it is not +part of CORE and would also require a C compiler to install), nor do I want to issue a +shell command to "stty" to hide the input because that would restrict the game to Linux/Unix. +This means it would have to be played on the honor system. + +However, if you want to try it, install the module "Term::ReadKey" ("sudo cpan -i Term::ReadKey" +if on Linux/Unix and you have root access). If the code finds that module, it will automatically +use it and hide the input ... and restore echoing input again when the games ends. If the module +is not found, input will be visible. diff --git a/56_Life_for_Two/perl/lifefortwo.pl b/56_Life_for_Two/perl/lifefortwo.pl new file mode 100644 index 00000000..0c50cdef --- /dev/null +++ b/56_Life_for_Two/perl/lifefortwo.pl @@ -0,0 +1,215 @@ +#!/usr/bin/perl + +# Life_For_Two program in Perl +# Required extensive restructuring to remove all of the GOTO's. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# try to load module to hide input, set RKey to true if found +my $Rkey = eval { require Term::ReadKey } // 0; +END { Term::ReadKey::ReadMode('normal') if ($Rkey); } + +# globals +my @Board; # 2D board +my @X; # ? +my @Y; # ? +my $Player; # 1 or 2 +my $M2 = 0; # ? +my $M3 = 0; # ? + +# add 0 on front to make data 1 based +my @K = (0,3,102,103,120,130,121,112,111,12,21,30,1020,1030,1011,1021,1003,1002,1012); +my @A = (0,-1,0,1,0,0,-1,0,1,-1,-1,1,-1,-1,1,1,1); + +print "\n"; +print " " x 33, "LIFE2\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; +print " " x 10, "U.B. LIFE GAME\n"; +for my $j (1 .. 5) { for my $k (1 .. 5) { $Board[$j][$k] = 0; } } + +for (1 .. 2) +{ + $Player = $_; # if we make $Player the loop var, the global isn't set + my $p1 = ($Player == 2) ? 30 : 3; + print "\nPLAYER $Player - 3 LIVE PIECES.\n"; + for (1 .. 3) + { + get_input(); + $Board[$X[$Player]][$Y[$Player]] = $p1 if ($Player != 99); + } +} +print_board(); # print board after initial input + +while (1) +{ + print "\n"; + calc_board(); # calc new positions + print_board(); # print current board after calc + + if ($M2 == 0 && $M3 == 0) + { + print "\nA DRAW\n"; + last; + } + if ($M3 == 0) + { + win(1); + last; + } + if ($M2 == 0) + { + win(2); + last; + } + + for (1 .. 2) + { + $Player = $_; # if we make $Player the loop var, the global isn't set + print "\n\nPLAYER $Player "; + get_input(); + last if ($Player == 99); + } + next if ($Player == 99); + + $Board[$X[1]][$Y[1]] = 100; + $Board[$X[2]][$Y[2]] = 1000; +} +exit(0); + +########################################################### + +sub win +{ + my $p = shift; + print "\nPLAYER $p IS THE WINNER\n"; +} + +sub calc_board +{ + for my $j (1 .. 5) + { + for my $k (1 .. 5) + { + if ($Board[$j][$k] > 99) + { + $Player = $Board[$j][$k] > 999 ? 10 : 1; + for (my $c = 1 ; $c <= 15 ; $c += 2) + { + $Board[$j+$A[$c]][$k+$A[$c+1]] = ($Board[$j+$A[$c]][$k+$A[$c+1]] // 0) + $Player; + } + } + } + } +} + +sub print_board +{ + $M2 = 0; + $M3 = 0; + for my $j (0 .. 6) + { + print "\n"; + for my $k (0 .. 6) + { + if ($j != 0 && $j != 6) + { + if ($k != 0 && $k != 6) + { + print_row($j, $k); + next; + } + if ($j == 6) + { + print "0\n"; + return; + } + print " $j "; + } + else + { + if ($k == 6) + { + print " 0 "; + last; + } + print " $k "; + } + } + } +} + +sub print_row +{ + my ($j, $k) = @_; + + if ($Board[$j][$k] >= 3) + { + my $c; + for $c (1 .. 18) + { + if ($Board[$j][$k] == $K[$c]) + { + if ($c <= 9) + { + $Board[$j][$k] = 100; + $M2++; + print " * "; + } + else + { + $Board[$j][$k] = 1000; + $M3++; + print " # "; + } + return; + } + } + } + $Board[$j][$k] = 0; + print " "; +} + +sub get_input +{ + while (1) + { + print "X,Y\n"; + my $ans; + + if ($Rkey) + { + # code to hide input + Term::ReadKey::ReadMode('noecho'); + $ans = Term::ReadKey::ReadLine(0); + Term::ReadKey::ReadMode('restore'); + print "\n"; # do this since the one entered was hidden + } + else + { + # normal, input visible + chomp($ans = <>); + } + + ($Y[$Player], $X[$Player]) = split(/[,\s]+/, $ans, 2); + if ($X[$Player] > 5 || $X[$Player] < 1 || $Y[$Player] > 5 || $Y[$Player] < 1) + { + print "ILLEGAL COORDS. RETYPE\n"; + next; + } + # this tells you the cell was already taken not zero it out, bug! + #if ($Board[$X[$Player]][$Y[$Player]] != 0) + #{ + # print "ILLEGAL COORDS. RETYPE\n"; + # next; + #} + last; + } + + return if ($Player == 1 || $X[1] != $X[2] || $Y[1] != $Y[2]); + + print "SAME COORD. SET TO 0\n"; + $Board[$X[$Player]+1][$Y[$Player]+1] = 0; + $Player = 99; +} diff --git a/72_Queen/csharp/Computer.cs b/72_Queen/csharp/Computer.cs new file mode 100644 index 00000000..459c7337 --- /dev/null +++ b/72_Queen/csharp/Computer.cs @@ -0,0 +1,34 @@ +namespace Queen; + +internal class Computer +{ + private static readonly HashSet _randomiseFrom = new() { 41, 44, 73, 75, 126, 127 }; + private static readonly HashSet _desirable = new() { 73, 75, 126, 127, 158 }; + private readonly IRandom _random; + + public Computer(IRandom random) + { + _random = random; + } + + public Position GetMove(Position from) + => from + (_randomiseFrom.Contains(from) ? _random.NextMove() : FindMove(from)); + + private Move FindMove(Position from) + { + for (int i = 7; i > 0; i--) + { + if (IsOptimal(Move.Left, out var move)) { return move; } + if (IsOptimal(Move.Down, out move)) { return move; } + if (IsOptimal(Move.DownLeft, out move)) { return move; } + + bool IsOptimal(Move direction, out Move move) + { + move = direction * i; + return _desirable.Contains(from + move); + } + } + + return _random.NextMove(); + } +} diff --git a/72_Queen/csharp/Game.cs b/72_Queen/csharp/Game.cs new file mode 100644 index 00000000..35b1dc9a --- /dev/null +++ b/72_Queen/csharp/Game.cs @@ -0,0 +1,57 @@ +namespace Queen; + +internal class Game +{ + private readonly IReadWrite _io; + private readonly IRandom _random; + private readonly Computer _computer; + + public Game(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + _computer = new Computer(random); + } + + internal void PlaySeries() + { + _io.Write(Streams.Title); + if (_io.ReadYesNo(Prompts.Instructions)) { _io.Write(Streams.Instructions); } + + while (true) + { + var result = PlayGame(); + _io.Write(result switch + { + Result.HumanForfeits => Streams.Forfeit, + Result.HumanWins => Streams.Congratulations, + Result.ComputerWins => Streams.IWin, + _ => throw new InvalidOperationException($"Unexpected result {result}") + }); + + if (!_io.ReadYesNo(Prompts.Anyone)) { break; } + } + + _io.Write(Streams.Thanks); + } + + private Result PlayGame() + { + _io.Write(Streams.Board); + var humanPosition = _io.ReadPosition(Prompts.Start, p => p.IsStart, Streams.IllegalStart, repeatPrompt: true); + if (humanPosition.IsZero) { return Result.HumanForfeits; } + + while (true) + { + var computerPosition = _computer.GetMove(humanPosition); + _io.Write(Strings.ComputerMove(computerPosition)); + if (computerPosition.IsEnd) { return Result.ComputerWins; } + + humanPosition = _io.ReadPosition(Prompts.Move, p => (p - computerPosition).IsValid, Streams.IllegalMove); + if (humanPosition.IsZero) { return Result.HumanForfeits; } + if (humanPosition.IsEnd) { return Result.HumanWins; } + } + } + + private enum Result { ComputerWins, HumanWins, HumanForfeits }; +} diff --git a/72_Queen/csharp/IOExtensions.cs b/72_Queen/csharp/IOExtensions.cs new file mode 100644 index 00000000..51959967 --- /dev/null +++ b/72_Queen/csharp/IOExtensions.cs @@ -0,0 +1,38 @@ +namespace Queen; + +internal static class IOExtensions +{ + internal static bool ReadYesNo(this IReadWrite io, string prompt) + { + while (true) + { + var answer = io.ReadString(prompt).ToLower(); + if (answer == "yes") { return true; } + if (answer == "no") { return false; } + + io.Write(Streams.YesOrNo); + } + } + + internal static Position ReadPosition( + this IReadWrite io, + string prompt, + Predicate isValid, + Stream error, + bool repeatPrompt = false) + { + while (true) + { + var response = io.ReadNumber(prompt); + var number = (int)response; + var position = new Position(number); + if (number == response && (position.IsZero || isValid(position))) + { + return position; + } + + io.Write(error); + if (!repeatPrompt) { prompt = ""; } + } + } +} diff --git a/72_Queen/csharp/Move.cs b/72_Queen/csharp/Move.cs new file mode 100644 index 00000000..4e18647b --- /dev/null +++ b/72_Queen/csharp/Move.cs @@ -0,0 +1,15 @@ +namespace Queen; + +internal record struct Move(int Diagonal, int Row) +{ + public static readonly Move Left = new(1, 0); + public static readonly Move DownLeft = new(2, 1); + public static readonly Move Down = new(1, 1); + + public bool IsValid => Diagonal > 0 && (IsLeft || IsDown || IsDownLeft); + private bool IsLeft => Row == 0; + private bool IsDown => Row == Diagonal; + private bool IsDownLeft => Row * 2 == Diagonal; + + public static Move operator *(Move move, int scale) => new(move.Diagonal * scale, move.Row * scale); +} \ No newline at end of file diff --git a/72_Queen/csharp/Position.cs b/72_Queen/csharp/Position.cs new file mode 100644 index 00000000..69971163 --- /dev/null +++ b/72_Queen/csharp/Position.cs @@ -0,0 +1,24 @@ +namespace Queen; + +internal record struct Position(int Diagonal, int Row) +{ + public static readonly Position Zero = new(0); + + public Position(int number) + : this(Diagonal: number / 10, Row: number % 10) + { + } + + public bool IsZero => Row == 0 && Diagonal == 0; + public bool IsStart => Row == 1 || Row == Diagonal; + public bool IsEnd => Row == 8 && Diagonal == 15; + + public override string ToString() => $"{Diagonal}{Row}"; + + public static implicit operator Position(int value) => new(value); + + public static Position operator +(Position position, Move move) + => new(Diagonal: position.Diagonal + move.Diagonal, Row: position.Row + move.Row); + public static Move operator -(Position to, Position from) + => new(Diagonal: to.Diagonal - from.Diagonal, Row: to.Row - from.Row); +} diff --git a/72_Queen/csharp/Program.cs b/72_Queen/csharp/Program.cs new file mode 100644 index 00000000..5e017df6 --- /dev/null +++ b/72_Queen/csharp/Program.cs @@ -0,0 +1,7 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; +global using static Queen.Resources.Resource; + +using Queen; + +new Game(new ConsoleIO(), new RandomNumberGenerator()).PlaySeries(); \ No newline at end of file diff --git a/72_Queen/csharp/Queen.csproj b/72_Queen/csharp/Queen.csproj index d3fe4757..3870320c 100644 --- a/72_Queen/csharp/Queen.csproj +++ b/72_Queen/csharp/Queen.csproj @@ -6,4 +6,12 @@ enable enable + + + + + + + + diff --git a/72_Queen/csharp/RandomExtensions.cs b/72_Queen/csharp/RandomExtensions.cs new file mode 100644 index 00000000..b4e375fd --- /dev/null +++ b/72_Queen/csharp/RandomExtensions.cs @@ -0,0 +1,12 @@ +namespace Queen; + +internal static class RandomExtensions +{ + internal static Move NextMove(this IRandom random) + => random.NextFloat() switch + { + > 0.6F => Move.Down, + > 0.3F => Move.DownLeft, + _ => Move.Left + }; +} diff --git a/72_Queen/csharp/Resources/AnyonePrompt.txt b/72_Queen/csharp/Resources/AnyonePrompt.txt new file mode 100644 index 00000000..a0289fd6 --- /dev/null +++ b/72_Queen/csharp/Resources/AnyonePrompt.txt @@ -0,0 +1 @@ +Anyone else care to try \ No newline at end of file diff --git a/72_Queen/csharp/Resources/Board.txt b/72_Queen/csharp/Resources/Board.txt new file mode 100644 index 00000000..854be1bb --- /dev/null +++ b/72_Queen/csharp/Resources/Board.txt @@ -0,0 +1,27 @@ + + + 81 71 61 51 41 31 21 11 + + + 92 82 72 62 52 42 32 22 + + + 103 93 83 73 63 53 43 33 + + + 114 104 94 84 74 64 54 44 + + + 125 115 105 95 85 75 65 55 + + + 136 126 116 106 96 86 76 66 + + + 147 137 127 117 107 97 87 77 + + + 158 148 138 128 118 108 98 88 + + + diff --git a/72_Queen/csharp/Resources/ComputerMove.txt b/72_Queen/csharp/Resources/ComputerMove.txt new file mode 100644 index 00000000..f31b8e32 --- /dev/null +++ b/72_Queen/csharp/Resources/ComputerMove.txt @@ -0,0 +1 @@ +Computer moves to square {0} diff --git a/72_Queen/csharp/Resources/Congratulations.txt b/72_Queen/csharp/Resources/Congratulations.txt new file mode 100644 index 00000000..9f1db232 --- /dev/null +++ b/72_Queen/csharp/Resources/Congratulations.txt @@ -0,0 +1,7 @@ + +C O N G R A T U L A T I O N S . . . + +You have won--very well played. +It looks like I have met my match. +Thanks for playing--I can't win all the time. + diff --git a/72_Queen/csharp/Resources/Forfeit.txt b/72_Queen/csharp/Resources/Forfeit.txt new file mode 100644 index 00000000..09858bc1 --- /dev/null +++ b/72_Queen/csharp/Resources/Forfeit.txt @@ -0,0 +1,3 @@ + +It looks like I have won by forfeit. + diff --git a/72_Queen/csharp/Resources/IWin.txt b/72_Queen/csharp/Resources/IWin.txt new file mode 100644 index 00000000..ced5d716 --- /dev/null +++ b/72_Queen/csharp/Resources/IWin.txt @@ -0,0 +1,4 @@ + +Nice try, but it looks like I have won. +Thanks for playing. + diff --git a/72_Queen/csharp/Resources/IllegalMove.txt b/72_Queen/csharp/Resources/IllegalMove.txt new file mode 100644 index 00000000..ae4a3bbc --- /dev/null +++ b/72_Queen/csharp/Resources/IllegalMove.txt @@ -0,0 +1,2 @@ + +Y O U C H E A T . . . Try again \ No newline at end of file diff --git a/72_Queen/csharp/Resources/IllegalStart.txt b/72_Queen/csharp/Resources/IllegalStart.txt new file mode 100644 index 00000000..25402bb6 --- /dev/null +++ b/72_Queen/csharp/Resources/IllegalStart.txt @@ -0,0 +1,3 @@ +Please read the instructions again. +You have begun illegally. + diff --git a/72_Queen/csharp/Resources/Instructions.txt b/72_Queen/csharp/Resources/Instructions.txt new file mode 100644 index 00000000..fc2e85b0 --- /dev/null +++ b/72_Queen/csharp/Resources/Instructions.txt @@ -0,0 +1,15 @@ +We are going to play a game based on one of the chess +moves. Our queen will be able to move only to the left, +down, or diagonally down and to the left. + +The object of the game is to place the queen in the lower +left hand square by alternating moves between you and the +computer. The first one to place the queen there wins. + +You go first and place the queen in any one of the squares +on the top row or right hand column. +That will be your first move. +We alternate moves. +You may forfeit by typing '0' as your move. +Be sure to press the return key after each response. + diff --git a/72_Queen/csharp/Resources/InstructionsPrompt.txt b/72_Queen/csharp/Resources/InstructionsPrompt.txt new file mode 100644 index 00000000..0d311b60 --- /dev/null +++ b/72_Queen/csharp/Resources/InstructionsPrompt.txt @@ -0,0 +1 @@ +Do you want instructions \ No newline at end of file diff --git a/72_Queen/csharp/Resources/MovePrompt.txt b/72_Queen/csharp/Resources/MovePrompt.txt new file mode 100644 index 00000000..8cb18999 --- /dev/null +++ b/72_Queen/csharp/Resources/MovePrompt.txt @@ -0,0 +1 @@ +What is your move \ No newline at end of file diff --git a/72_Queen/csharp/Resources/Resource.cs b/72_Queen/csharp/Resources/Resource.cs new file mode 100644 index 00000000..e8297eca --- /dev/null +++ b/72_Queen/csharp/Resources/Resource.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Queen.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Title => GetStream(); + public static Stream Instructions => GetStream(); + public static Stream YesOrNo => GetStream(); + public static Stream Board => GetStream(); + public static Stream IllegalStart => GetStream(); + public static Stream IllegalMove => GetStream(); + public static Stream Forfeit => GetStream(); + public static Stream IWin => GetStream(); + public static Stream Congratulations => GetStream(); + public static Stream Thanks => GetStream(); + } + + internal static class Prompts + { + public static string Instructions => GetPrompt(); + public static string Start => GetPrompt(); + public static string Move => GetPrompt(); + public static string Anyone => GetPrompt(); + } + + internal static class Strings + { + public static string ComputerMove(Position position) => string.Format(GetString(), position); + } + + private static string GetPrompt([CallerMemberName] string? name = null) => GetString($"{name}Prompt"); + + private static string GetString([CallerMemberName] string? name = null) + { + using var stream = GetStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/72_Queen/csharp/Resources/StartPrompt.txt b/72_Queen/csharp/Resources/StartPrompt.txt new file mode 100644 index 00000000..0b6f395b --- /dev/null +++ b/72_Queen/csharp/Resources/StartPrompt.txt @@ -0,0 +1 @@ +Where would you like to start \ No newline at end of file diff --git a/72_Queen/csharp/Resources/Thanks.txt b/72_Queen/csharp/Resources/Thanks.txt new file mode 100644 index 00000000..2e2e7b63 --- /dev/null +++ b/72_Queen/csharp/Resources/Thanks.txt @@ -0,0 +1,3 @@ + + +Ok --- thanks again. \ No newline at end of file diff --git a/72_Queen/csharp/Resources/Title.txt b/72_Queen/csharp/Resources/Title.txt new file mode 100644 index 00000000..63549d09 --- /dev/null +++ b/72_Queen/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Queen + Creative Computing Morristown, New Jersey + + + diff --git a/72_Queen/csharp/Resources/YesOrNo.txt b/72_Queen/csharp/Resources/YesOrNo.txt new file mode 100644 index 00000000..657bf8aa --- /dev/null +++ b/72_Queen/csharp/Resources/YesOrNo.txt @@ -0,0 +1 @@ +Please answer 'Yes' or 'No'. diff --git a/75_Roulette/csharp/Bet.cs b/75_Roulette/csharp/Bet.cs new file mode 100644 index 00000000..da0e31b7 --- /dev/null +++ b/75_Roulette/csharp/Bet.cs @@ -0,0 +1,6 @@ +namespace Roulette; + +internal record struct Bet(BetType Type, int Number, int Wager) +{ + public int Payout => Wager * Type.Payout; +} diff --git a/75_Roulette/csharp/BetType.cs b/75_Roulette/csharp/BetType.cs new file mode 100644 index 00000000..79b3b67c --- /dev/null +++ b/75_Roulette/csharp/BetType.cs @@ -0,0 +1,13 @@ +namespace Roulette; + +internal record struct BetType(int Value) +{ + public static implicit operator BetType(int value) => new(value); + + public int Payout => Value switch + { + <= 36 or >= 49 => 35, + <= 42 => 2, + <= 48 => 1 + }; +} diff --git a/75_Roulette/csharp/Croupier.cs b/75_Roulette/csharp/Croupier.cs new file mode 100644 index 00000000..32f76c33 --- /dev/null +++ b/75_Roulette/csharp/Croupier.cs @@ -0,0 +1,41 @@ +namespace Roulette; + +internal class Croupier +{ + private const int _initialHouse = 100_000; + private const int _initialPlayer = 1_000; + + private int _house = _initialHouse; + private int _player = _initialPlayer; + + public string Totals => Strings.Totals(_house, _player); + public bool PlayerIsBroke => _player <= 0; + public bool HouseIsBroke => _house <= 0; + + internal string Pay(Bet bet) + { + _house -= bet.Payout; + _player += bet.Payout; + + if (_house <= 0) + { + _player = _initialHouse + _initialPlayer; + } + + return Strings.Win(bet); + } + + internal string Take(Bet bet) + { + _house += bet.Wager; + _player -= bet.Wager; + + return Strings.Lose(bet); + } + + public void CutCheck(IReadWrite io, IRandom random) + { + var name = io.ReadString(Prompts.Check); + io.Write(Strings.Check(random, name, _player)); + } +} diff --git a/75_Roulette/csharp/Game.cs b/75_Roulette/csharp/Game.cs new file mode 100644 index 00000000..75a6d7e5 --- /dev/null +++ b/75_Roulette/csharp/Game.cs @@ -0,0 +1,42 @@ +namespace Roulette; + +internal class Game +{ + private readonly IReadWrite _io; + private readonly IRandom _random; + private readonly Table _table; + private readonly Croupier _croupier; + + public Game(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + _croupier = new(); + _table = new(_croupier, io, random); + } + + public void Play() + { + _io.Write(Streams.Title); + if (!_io.ReadString(Prompts.Instructions).ToLowerInvariant().StartsWith('n')) + { + _io.Write(Streams.Instructions); + } + + while (_table.Play()); + + if (_croupier.PlayerIsBroke) + { + _io.Write(Streams.LastDollar); + _io.Write(Streams.Thanks); + return; + } + + if (_croupier.HouseIsBroke) + { + _io.Write(Streams.BrokeHouse); + } + + _croupier.CutCheck(_io, _random); + } +} diff --git a/75_Roulette/csharp/IOExtensions.cs b/75_Roulette/csharp/IOExtensions.cs new file mode 100644 index 00000000..49326bef --- /dev/null +++ b/75_Roulette/csharp/IOExtensions.cs @@ -0,0 +1,34 @@ +namespace Roulette; + +internal static class IOExtensions +{ + internal static int ReadBetCount(this IReadWrite io) + { + while (true) + { + var betCount = io.ReadNumber(Prompts.HowManyBets); + if (betCount.IsValidInt(1)) { return (int)betCount; } + } + } + + internal static Bet ReadBet(this IReadWrite io, int number) + { + while (true) + { + var (type, amount) = io.Read2Numbers(Prompts.Bet(number)); + + if (type.IsValidInt(1, 50) && amount.IsValidInt(5, 500)) + { + return new() + { + Type = (int)type, + Number = number, + Wager = (int)amount + }; + } + } + } + + internal static bool IsValidInt(this float value, int minValue, int maxValue = int.MaxValue) + => value == (int)value && value >= minValue && value <= maxValue; +} \ No newline at end of file diff --git a/75_Roulette/csharp/Program.cs b/75_Roulette/csharp/Program.cs new file mode 100644 index 00000000..4be74962 --- /dev/null +++ b/75_Roulette/csharp/Program.cs @@ -0,0 +1,6 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; +global using static Roulette.Resources.Resource; +using Roulette; + +new Game(new ConsoleIO(), new RandomNumberGenerator()).Play(); diff --git a/75_Roulette/csharp/Resources/AgainPrompt.txt b/75_Roulette/csharp/Resources/AgainPrompt.txt new file mode 100644 index 00000000..bd1d18b6 --- /dev/null +++ b/75_Roulette/csharp/Resources/AgainPrompt.txt @@ -0,0 +1 @@ +Again \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/BetAlready.txt b/75_Roulette/csharp/Resources/BetAlready.txt new file mode 100644 index 00000000..be86cbea --- /dev/null +++ b/75_Roulette/csharp/Resources/BetAlready.txt @@ -0,0 +1 @@ +You made that bet once already,dum-dum \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/BetPrompt.txt b/75_Roulette/csharp/Resources/BetPrompt.txt new file mode 100644 index 00000000..50039840 --- /dev/null +++ b/75_Roulette/csharp/Resources/BetPrompt.txt @@ -0,0 +1 @@ +Number {0} \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/BrokeHouse.txt b/75_Roulette/csharp/Resources/BrokeHouse.txt new file mode 100644 index 00000000..638dafb4 --- /dev/null +++ b/75_Roulette/csharp/Resources/BrokeHouse.txt @@ -0,0 +1 @@ +You broke the house! diff --git a/75_Roulette/csharp/Resources/Check.txt b/75_Roulette/csharp/Resources/Check.txt new file mode 100644 index 00000000..ffdd640a --- /dev/null +++ b/75_Roulette/csharp/Resources/Check.txt @@ -0,0 +1,16 @@ + +------------------------------------------------------------------------Check No. {0} + + {1:MMMM d',' yyyy} + + +Pay to the order of-----{2}-----$ {3} + + + The Memory Bank of New YORK + + The Computer + ----------X----- + +--------------------------------------------------------------Come back soon! + diff --git a/75_Roulette/csharp/Resources/CheckPrompt.txt b/75_Roulette/csharp/Resources/CheckPrompt.txt new file mode 100644 index 00000000..16321fe9 --- /dev/null +++ b/75_Roulette/csharp/Resources/CheckPrompt.txt @@ -0,0 +1 @@ +To whom shall I make the check \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/HowManyBetsPrompt.txt b/75_Roulette/csharp/Resources/HowManyBetsPrompt.txt new file mode 100644 index 00000000..0b6abdfa --- /dev/null +++ b/75_Roulette/csharp/Resources/HowManyBetsPrompt.txt @@ -0,0 +1 @@ +How many bets \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/Instructions.txt b/75_Roulette/csharp/Resources/Instructions.txt new file mode 100644 index 00000000..8ced724b --- /dev/null +++ b/75_Roulette/csharp/Resources/Instructions.txt @@ -0,0 +1,48 @@ + +This is the betting layout + (*=RED) + + 1* 2 3* + 4 5* 6 + 7* 8 9* +10 11 12* +--------------- +13 14* 15 +16* 17 18* +19* 20 21* +22 23* 24 +--------------- +25* 26 27* +28 29 30* +31 32* 33 +34* 35 36* +--------------- + 00 0 + +Types of bets + +The numbers 1 to 36 signify a straight bet +on that number. +These pay off 35:1 + +The 2:1 bets are: + 37) 1-12 40) First column + 38) 13-24 41) Second column + 39) 25-36 42) Third column + +The even money bets are: + 43) 1-18 46) Odd + 44) 19-36 47) Red + 45) Even 48) Black + +49)0 and 50)00 pay off 35:1 +Note: 0 and 00 do not count under any + bets except their own. + +When I ask for each bet, type the number +and the amount, separated by a comma. +For example: to bet $500 on Black, type 48,500 +when I ask for a bet. + +The minimum bet is $5, the maximum is $500. + diff --git a/75_Roulette/csharp/Resources/InstructionsPrompt.txt b/75_Roulette/csharp/Resources/InstructionsPrompt.txt new file mode 100644 index 00000000..0d311b60 --- /dev/null +++ b/75_Roulette/csharp/Resources/InstructionsPrompt.txt @@ -0,0 +1 @@ +Do you want instructions \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/LastDollar.txt b/75_Roulette/csharp/Resources/LastDollar.txt new file mode 100644 index 00000000..632516de --- /dev/null +++ b/75_Roulette/csharp/Resources/LastDollar.txt @@ -0,0 +1 @@ +Oops! you just spent your last dollar! diff --git a/75_Roulette/csharp/Resources/Outcome.txt b/75_Roulette/csharp/Resources/Outcome.txt new file mode 100644 index 00000000..30e227f7 --- /dev/null +++ b/75_Roulette/csharp/Resources/Outcome.txt @@ -0,0 +1 @@ +You {0} {1} dollars on bet {2} diff --git a/75_Roulette/csharp/Resources/Resource.cs b/75_Roulette/csharp/Resources/Resource.cs new file mode 100644 index 00000000..dd9c8672 --- /dev/null +++ b/75_Roulette/csharp/Resources/Resource.cs @@ -0,0 +1,59 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using Games.Common.Randomness; + +namespace Roulette.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Title => GetStream(); + public static Stream Instructions => GetStream(); + public static Stream BetAlready => GetStream(); + public static Stream Spinning => GetStream(); + public static Stream LastDollar => GetStream(); + public static Stream BrokeHouse => GetStream(); + public static Stream Thanks => GetStream(); + } + + internal static class Strings + { + public static string Black(int number) => Slot(number); + public static string Red(int number) => Slot(number); + private static string Slot(int number, [CallerMemberName] string? colour = null) + => string.Format(GetString(), number, colour); + + public static string Lose(Bet bet) => Outcome(bet.Wager, bet.Number); + public static string Win(Bet bet) => Outcome(bet.Payout, bet.Number); + private static string Outcome(int amount, int number, [CallerMemberName] string? winlose = null) + => string.Format(GetString(), winlose, amount, number); + + public static string Totals(int me, int you) => string.Format(GetString(), me, you); + + public static string Check(IRandom random, string payee, int amount) + => string.Format(GetString(), random.Next(100), DateTime.Now, payee, amount); + } + + internal static class Prompts + { + public static string Instructions => GetPrompt(); + public static string HowManyBets => GetPrompt(); + public static string Bet(int number) => string.Format(GetPrompt(), number); + public static string Again => GetPrompt(); + public static string Check => GetPrompt(); + } + + private static string GetPrompt([CallerMemberName] string? name = null) => GetString($"{name}Prompt"); + + private static string GetString([CallerMemberName] string? name = null) + { + using var stream = GetStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/75_Roulette/csharp/Resources/Slot.txt b/75_Roulette/csharp/Resources/Slot.txt new file mode 100644 index 00000000..de02695d --- /dev/null +++ b/75_Roulette/csharp/Resources/Slot.txt @@ -0,0 +1,2 @@ + {0} {1} + diff --git a/75_Roulette/csharp/Resources/Spinning.txt b/75_Roulette/csharp/Resources/Spinning.txt new file mode 100644 index 00000000..0d87fe39 --- /dev/null +++ b/75_Roulette/csharp/Resources/Spinning.txt @@ -0,0 +1,3 @@ +Spinning + + diff --git a/75_Roulette/csharp/Resources/Thanks.txt b/75_Roulette/csharp/Resources/Thanks.txt new file mode 100644 index 00000000..0b835237 --- /dev/null +++ b/75_Roulette/csharp/Resources/Thanks.txt @@ -0,0 +1,3 @@ +Thanks for you money. +I'll use it to buy a solid gold roulette WHEEL + diff --git a/75_Roulette/csharp/Resources/Title.txt b/75_Roulette/csharp/Resources/Title.txt new file mode 100644 index 00000000..0d53f1a8 --- /dev/null +++ b/75_Roulette/csharp/Resources/Title.txt @@ -0,0 +1,7 @@ + Roulette + Creative Computing Morristown, New Jersey + + + +Welcome to the roulette table + diff --git a/75_Roulette/csharp/Resources/Totals.txt b/75_Roulette/csharp/Resources/Totals.txt new file mode 100644 index 00000000..4cecae3f --- /dev/null +++ b/75_Roulette/csharp/Resources/Totals.txt @@ -0,0 +1,3 @@ + +Totals Me You + {0,-14}{1} diff --git a/75_Roulette/csharp/Roulette.csproj b/75_Roulette/csharp/Roulette.csproj index d3fe4757..23d27b76 100644 --- a/75_Roulette/csharp/Roulette.csproj +++ b/75_Roulette/csharp/Roulette.csproj @@ -1,9 +1,17 @@ Exe - net6.0 + net7.0 10 enable enable + + + + + + + + diff --git a/75_Roulette/csharp/Slot.cs b/75_Roulette/csharp/Slot.cs new file mode 100644 index 00000000..df30cea1 --- /dev/null +++ b/75_Roulette/csharp/Slot.cs @@ -0,0 +1,18 @@ +using System.Collections.Immutable; + +namespace Roulette; + +internal class Slot +{ + private readonly ImmutableHashSet _coveringBets; + + public Slot (string name, params BetType[] coveringBets) + { + Name = name; + _coveringBets = coveringBets.ToImmutableHashSet(); + } + + public string Name { get; } + + public bool IsCoveredBy(Bet bet) => _coveringBets.Contains(bet.Type); +} diff --git a/75_Roulette/csharp/Table.cs b/75_Roulette/csharp/Table.cs new file mode 100644 index 00000000..026152d1 --- /dev/null +++ b/75_Roulette/csharp/Table.cs @@ -0,0 +1,71 @@ +namespace Roulette; + +internal class Table +{ + private readonly IReadWrite _io; + private readonly Wheel _wheel; + private readonly Croupier _croupier; + + public Table(Croupier croupier, IReadWrite io, IRandom random) + { + _croupier = croupier; + _io = io; + _wheel = new(random); + } + + public bool Play() + { + var bets = AcceptBets(); + var slot = SpinWheel(); + SettleBets(bets, slot); + + _io.Write(_croupier.Totals); + + if (_croupier.PlayerIsBroke || _croupier.HouseIsBroke) { return false; } + + return _io.ReadString(Prompts.Again).ToLowerInvariant().StartsWith('y'); + } + + private Slot SpinWheel() + { + _io.Write(Streams.Spinning); + var slot = _wheel.Spin(); + _io.Write(slot.Name); + return slot; + } + + private IReadOnlyList AcceptBets() + { + var betCount = _io.ReadBetCount(); + var betTypes = new HashSet(); + var bets = new List(); + for (int i = 1; i <= betCount; i++) + { + while (!TryAdd(_io.ReadBet(i))) + { + _io.Write(Streams.BetAlready); + } + } + + return bets.AsReadOnly(); + + bool TryAdd(Bet bet) + { + if (betTypes.Add(bet.Type)) + { + bets.Add(bet); + return true; + } + + return false; + } + } + + private void SettleBets(IReadOnlyList bets, Slot slot) + { + foreach (var bet in bets) + { + _io.Write(slot.IsCoveredBy(bet) ? _croupier.Pay(bet) : _croupier.Take(bet)); + } + } +} diff --git a/75_Roulette/csharp/Wheel.cs b/75_Roulette/csharp/Wheel.cs new file mode 100644 index 00000000..dfcecd29 --- /dev/null +++ b/75_Roulette/csharp/Wheel.cs @@ -0,0 +1,52 @@ +using System.Collections.Immutable; + +namespace Roulette; + +internal class Wheel +{ + private static readonly ImmutableArray _slots = ImmutableArray.Create( + new Slot(Strings.Red(1), 1, 37, 40, 43, 46, 47), + new Slot(Strings.Black(2), 2, 37, 41, 43, 45, 48), + new Slot(Strings.Red(3), 3, 37, 42, 43, 46, 47), + new Slot(Strings.Black(4), 4, 37, 40, 43, 45, 48), + new Slot(Strings.Red(5), 5, 37, 41, 43, 46, 47), + new Slot(Strings.Black(6), 6, 37, 42, 43, 45, 48), + new Slot(Strings.Red(7), 7, 37, 40, 43, 46, 47), + new Slot(Strings.Black(8), 8, 37, 41, 43, 45, 48), + new Slot(Strings.Red(9), 9, 37, 42, 43, 46, 47), + new Slot(Strings.Black(10), 10, 37, 40, 43, 45, 48), + new Slot(Strings.Black(11), 11, 37, 41, 43, 46, 48), + new Slot(Strings.Red(12), 12, 37, 42, 43, 45, 47), + new Slot(Strings.Black(13), 13, 38, 40, 43, 46, 48), + new Slot(Strings.Red(14), 14, 38, 41, 43, 45, 47), + new Slot(Strings.Black(15), 15, 38, 42, 43, 46, 48), + new Slot(Strings.Red(16), 16, 38, 40, 43, 45, 47), + new Slot(Strings.Black(17), 17, 38, 41, 43, 46, 48), + new Slot(Strings.Red(18), 18, 38, 42, 43, 45, 47), + new Slot(Strings.Red(19), 19, 38, 40, 44, 46, 47), + new Slot(Strings.Black(20), 20, 38, 41, 44, 45, 48), + new Slot(Strings.Red(21), 21, 38, 42, 44, 46, 47), + new Slot(Strings.Black(22), 22, 38, 40, 44, 45, 48), + new Slot(Strings.Red(23), 23, 38, 41, 44, 46, 47), + new Slot(Strings.Black(24), 24, 38, 42, 44, 45, 48), + new Slot(Strings.Red(25), 25, 39, 40, 44, 46, 47), + new Slot(Strings.Black(26), 26, 39, 41, 44, 45, 48), + new Slot(Strings.Red(27), 27, 39, 42, 44, 46, 47), + new Slot(Strings.Black(28), 28, 39, 40, 44, 45, 48), + new Slot(Strings.Black(29), 29, 39, 41, 44, 46, 48), + new Slot(Strings.Red(30), 30, 39, 42, 44, 45, 47), + new Slot(Strings.Black(31), 31, 39, 40, 44, 46, 48), + new Slot(Strings.Red(32), 32, 39, 41, 44, 45, 47), + new Slot(Strings.Black(33), 33, 39, 42, 44, 46, 48), + new Slot(Strings.Red(34), 34, 39, 40, 44, 45, 47), + new Slot(Strings.Black(35), 35, 39, 41, 44, 46, 48), + new Slot(Strings.Red(36), 36, 39, 42, 44, 45, 47), + new Slot("0", 49), + new Slot("00", 50)); + + private readonly IRandom _random; + + public Wheel(IRandom random) => _random = random; + + public Slot Spin() => _slots[_random.Next(_slots.Length)]; +} diff --git a/77_Salvo/csharp/Coordinate.cs b/77_Salvo/csharp/Coordinate.cs new file mode 100644 index 00000000..4cd2c433 --- /dev/null +++ b/77_Salvo/csharp/Coordinate.cs @@ -0,0 +1,45 @@ +namespace Salvo; + +internal record struct Coordinate(int Value) +{ + public const int MinValue = 1; + public const int MaxValue = 10; + + public static IEnumerable Range => Enumerable.Range(1, 10).Select(v => new Coordinate(v)); + + public bool IsInRange => Value is >= MinValue and <= MaxValue; + + public static Coordinate Create(float value) => new((int)value); + + public static bool TryCreateValid(float value, out Coordinate coordinate) + { + coordinate = default; + if (value != (int)value) { return false; } + + var result = Create(value); + + if (result.IsInRange) + { + coordinate = result; + return true; + } + + return false; + } + + public Coordinate BringIntoRange(IRandom random) + => Value switch + { + < MinValue => new(MinValue + (int)random.NextFloat(2.5F)), + > MaxValue => new(MaxValue - (int)random.NextFloat(2.5F)), + _ => this + }; + + public static implicit operator Coordinate(float value) => Create(value); + public static implicit operator int(Coordinate coordinate) => coordinate.Value; + + public static Coordinate operator +(Coordinate coordinate, int offset) => new(coordinate.Value + offset); + public static int operator -(Coordinate a, Coordinate b) => a.Value - b.Value; + + public override string ToString() => $" {Value} "; +} diff --git a/77_Salvo/csharp/Extensions/IOExtensions.cs b/77_Salvo/csharp/Extensions/IOExtensions.cs new file mode 100644 index 00000000..6d021deb --- /dev/null +++ b/77_Salvo/csharp/Extensions/IOExtensions.cs @@ -0,0 +1,27 @@ +namespace Games.Common.IO; + +internal static class IOExtensions +{ + internal static Position ReadPosition(this IReadWrite io) => Position.Create(io.Read2Numbers("")); + + internal static Position ReadValidPosition(this IReadWrite io) + { + while (true) + { + if (Position.TryCreateValid(io.Read2Numbers(""), out var position)) + { + return position; + } + io.Write(Streams.Illegal); + } + } + + internal static IEnumerable ReadPositions(this IReadWrite io, string shipName, int shipSize) + { + io.WriteLine(shipName); + for (var i = 0; i < shipSize; i++) + { + yield return io.ReadPosition(); + } + } +} diff --git a/77_Salvo/csharp/Extensions/RandomExtensions.cs b/77_Salvo/csharp/Extensions/RandomExtensions.cs new file mode 100644 index 00000000..e83ab265 --- /dev/null +++ b/77_Salvo/csharp/Extensions/RandomExtensions.cs @@ -0,0 +1,32 @@ +namespace Games.Common.Randomness; + +internal static class RandomExtensions +{ + internal static (Position, Offset) NextShipPosition(this IRandom random) + { + var startX = random.NextCoordinate(); + var startY = random.NextCoordinate(); + var deltaY = random.NextOffset(); + var deltaX = random.NextOffset(); + return (new(startX, startY), new(deltaX, deltaY)); + } + + private static Coordinate NextCoordinate(this IRandom random) + => random.Next(Coordinate.MinValue, Coordinate.MaxValue + 1); + + private static int NextOffset(this IRandom random) => random.Next(-1, 2); + + internal static (Position, Offset) GetRandomShipPositionInRange(this IRandom random, int shipSize) + { + while (true) + { + var (start, delta) = random.NextShipPosition(); + var shipSizeLessOne = shipSize - 1; + var end = start + delta * shipSizeLessOne; + if (delta != 0 && end.IsInRange) + { + return (start, delta); + } + } + } +} diff --git a/77_Salvo/csharp/Fleet.cs b/77_Salvo/csharp/Fleet.cs new file mode 100644 index 00000000..5f267225 --- /dev/null +++ b/77_Salvo/csharp/Fleet.cs @@ -0,0 +1,66 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Salvo; + +internal class Fleet +{ + private readonly List _ships; + + internal Fleet(IReadWrite io) + { + io.WriteLine(Prompts.Coordinates); + _ships = new() + { + new Battleship(io), + new Cruiser(io), + new Destroyer("A", io), + new Destroyer("B", io) + }; + } + + internal Fleet(IRandom random) + { + _ships = new(); + while (true) + { + _ships.Add(new Battleship(random)); + if (TryPositionShip(() => new Cruiser(random)) && + TryPositionShip(() => new Destroyer("A", random)) && + TryPositionShip(() => new Destroyer("B", random))) + { + return; + } + _ships.Clear(); + } + + bool TryPositionShip(Func shipFactory) + { + var shipGenerationAttempts = 0; + while (true) + { + var ship = shipFactory.Invoke(); + shipGenerationAttempts++; + if (shipGenerationAttempts > 25) { return false; } + if (_ships.Min(ship.DistanceTo) >= 3.59) + { + _ships.Add(ship); + return true; + } + } + } + } + + internal IEnumerable Ships => _ships.AsEnumerable(); + + internal void ReceiveShots(IEnumerable shots, Action reportHit) + { + foreach (var position in shots) + { + var ship = _ships.FirstOrDefault(s => s.IsHit(position)); + if (ship == null) { continue; } + if (ship.IsDestroyed) { _ships.Remove(ship); } + reportHit(ship); + } + } +} diff --git a/77_Salvo/csharp/Game.cs b/77_Salvo/csharp/Game.cs new file mode 100644 index 00000000..a0223d8b --- /dev/null +++ b/77_Salvo/csharp/Game.cs @@ -0,0 +1,29 @@ +namespace Salvo; + +internal class Game +{ + private readonly IReadWrite _io; + private readonly IRandom _random; + + public Game(IReadWrite io, IRandom random) + { + _io = io; + _random = random; + } + + internal void Play() + { + _io.Write(Streams.Title); + + var turnHandler = new TurnHandler(_io, _random); + _io.WriteLine(); + + Winner? winner; + do + { + winner = turnHandler.PlayTurn(); + } while (winner == null); + + _io.Write(winner == Winner.Computer ? Streams.IWon : Streams.YouWon); + } +} diff --git a/77_Salvo/csharp/Offset.cs b/77_Salvo/csharp/Offset.cs new file mode 100644 index 00000000..3c873f10 --- /dev/null +++ b/77_Salvo/csharp/Offset.cs @@ -0,0 +1,25 @@ +namespace Salvo; + +internal record struct Offset(int X, int Y) +{ + public static readonly Offset Zero = 0; + + public static Offset operator *(Offset offset, int scale) => new(offset.X * scale, offset.Y * scale); + + public static implicit operator Offset(int value) => new(value, value); + + public static IEnumerable Units + { + get + { + for (int x = -1; x <= 1; x++) + { + for (int y = -1; y <= 1; y++) + { + var offset = new Offset(x, y); + if (offset != Zero) { yield return offset; } + } + } + } + } +} diff --git a/77_Salvo/csharp/Position.cs b/77_Salvo/csharp/Position.cs new file mode 100644 index 00000000..3bafe217 --- /dev/null +++ b/77_Salvo/csharp/Position.cs @@ -0,0 +1,52 @@ +namespace Salvo; + +internal record struct Position(Coordinate X, Coordinate Y) +{ + public bool IsInRange => X.IsInRange && Y.IsInRange; + public bool IsOnDiagonal => X == Y; + + public static Position Create((float X, float Y) coordinates) => new(coordinates.X, coordinates.Y); + + public static bool TryCreateValid((float X, float Y) coordinates, out Position position) + { + if (Coordinate.TryCreateValid(coordinates.X, out var x) && Coordinate.TryCreateValid(coordinates.Y, out var y)) + { + position = new(x, y); + return true; + } + + position = default; + return false; + } + + public static IEnumerable All + => Coordinate.Range.SelectMany(x => Coordinate.Range.Select(y => new Position(x, y))); + + public IEnumerable Neighbours + { + get + { + foreach (var offset in Offset.Units) + { + var neighbour = this + offset; + if (neighbour.IsInRange) { yield return neighbour; } + } + } + } + + internal float DistanceTo(Position other) + { + var (deltaX, deltaY) = (X - other.X, Y - other.Y); + return (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + } + + internal Position BringIntoRange(IRandom random) + => IsInRange ? this : new(X.BringIntoRange(random), Y.BringIntoRange(random)); + + public static Position operator +(Position position, Offset offset) + => new(position.X + offset.X, position.Y + offset.Y); + + public static implicit operator Position(int value) => new(value, value); + + public override string ToString() => $"{X}{Y}"; +} diff --git a/77_Salvo/csharp/Program.cs b/77_Salvo/csharp/Program.cs new file mode 100644 index 00000000..5dde4d05 --- /dev/null +++ b/77_Salvo/csharp/Program.cs @@ -0,0 +1,9 @@ +global using System; +global using Games.Common.IO; +global using Games.Common.Randomness; +global using Salvo; +global using Salvo.Ships; +global using static Salvo.Resources.Resource; + +//new Game(new ConsoleIO(), new RandomNumberGenerator()).Play(); +new Game(new ConsoleIO(), new DataRandom()).Play(); diff --git a/77_Salvo/csharp/Resources/Coordinates.txt b/77_Salvo/csharp/Resources/Coordinates.txt new file mode 100644 index 00000000..387d7a6b --- /dev/null +++ b/77_Salvo/csharp/Resources/Coordinates.txt @@ -0,0 +1 @@ +Enter coordinates for... diff --git a/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt b/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt new file mode 100644 index 00000000..a6f37110 --- /dev/null +++ b/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt @@ -0,0 +1 @@ +I have more shots than blank squares. diff --git a/77_Salvo/csharp/Resources/IHaveShots.txt b/77_Salvo/csharp/Resources/IHaveShots.txt new file mode 100644 index 00000000..9157e1a7 --- /dev/null +++ b/77_Salvo/csharp/Resources/IHaveShots.txt @@ -0,0 +1 @@ +I have {0} shots. diff --git a/77_Salvo/csharp/Resources/IHit.txt b/77_Salvo/csharp/Resources/IHit.txt new file mode 100644 index 00000000..3b43216c --- /dev/null +++ b/77_Salvo/csharp/Resources/IHit.txt @@ -0,0 +1 @@ +I hit your {0} diff --git a/77_Salvo/csharp/Resources/IWon.txt b/77_Salvo/csharp/Resources/IWon.txt new file mode 100644 index 00000000..b3f50634 --- /dev/null +++ b/77_Salvo/csharp/Resources/IWon.txt @@ -0,0 +1 @@ +I have won. diff --git a/77_Salvo/csharp/Resources/Illegal.txt b/77_Salvo/csharp/Resources/Illegal.txt new file mode 100644 index 00000000..6a71787a --- /dev/null +++ b/77_Salvo/csharp/Resources/Illegal.txt @@ -0,0 +1 @@ +Illegal, enter again. diff --git a/77_Salvo/csharp/Resources/Resource.cs b/77_Salvo/csharp/Resources/Resource.cs new file mode 100644 index 00000000..8be52fdb --- /dev/null +++ b/77_Salvo/csharp/Resources/Resource.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Salvo.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Title => GetStream(); + public static Stream YouHaveMoreShotsThanSquares => GetStream(); + public static Stream YouWon => GetStream(); + public static Stream IHaveMoreShotsThanSquares => GetStream(); + public static Stream IWon => GetStream(); + public static Stream Illegal => GetStream(); + } + + internal static class Strings + { + public static string WhereAreYourShips => GetString(); + public static string YouHaveShots(int number) => Format(number); + public static string IHaveShots(int number) => Format(number); + public static string YouHit(string shipName) => Format(shipName); + public static string IHit(string shipName) => Format(shipName); + public static string ShotBefore(int turnNumber) => Format(turnNumber); + public static string Turn(int number) => Format(number); + } + + internal static class Prompts + { + public static string Coordinates => GetString(); + public static string Start => GetString(); + public static string SeeShots => GetString(); + } + + private static string Format(T value, [CallerMemberName] string? name = null) + => string.Format(GetString(name), value); + + private static string GetString([CallerMemberName] string? name = null) + { + using var stream = GetStream(name); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/SeeShots.txt b/77_Salvo/csharp/Resources/SeeShots.txt new file mode 100644 index 00000000..7f608b0b --- /dev/null +++ b/77_Salvo/csharp/Resources/SeeShots.txt @@ -0,0 +1 @@ +Do you want to see my shots \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/ShotBefore.txt b/77_Salvo/csharp/Resources/ShotBefore.txt new file mode 100644 index 00000000..18e90fcb --- /dev/null +++ b/77_Salvo/csharp/Resources/ShotBefore.txt @@ -0,0 +1 @@ +You shot there before on turn {0} diff --git a/77_Salvo/csharp/Resources/Start.txt b/77_Salvo/csharp/Resources/Start.txt new file mode 100644 index 00000000..283a0fbb --- /dev/null +++ b/77_Salvo/csharp/Resources/Start.txt @@ -0,0 +1 @@ +Do you want to start \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/Title.txt b/77_Salvo/csharp/Resources/Title.txt new file mode 100644 index 00000000..6b2ba9b3 --- /dev/null +++ b/77_Salvo/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Salvo + Creative Computing Morristown, New Jersey + + + diff --git a/77_Salvo/csharp/Resources/Turn.txt b/77_Salvo/csharp/Resources/Turn.txt new file mode 100644 index 00000000..14bf316e --- /dev/null +++ b/77_Salvo/csharp/Resources/Turn.txt @@ -0,0 +1,2 @@ + +Turn {0} diff --git a/77_Salvo/csharp/Resources/WhereAreYourShips.txt b/77_Salvo/csharp/Resources/WhereAreYourShips.txt new file mode 100644 index 00000000..e8391a46 --- /dev/null +++ b/77_Salvo/csharp/Resources/WhereAreYourShips.txt @@ -0,0 +1 @@ +Where are your ships? \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt b/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt new file mode 100644 index 00000000..04d58f92 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt @@ -0,0 +1 @@ +You have more shots than there are blank squares. diff --git a/77_Salvo/csharp/Resources/YouHaveShots.txt b/77_Salvo/csharp/Resources/YouHaveShots.txt new file mode 100644 index 00000000..43abeda8 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHaveShots.txt @@ -0,0 +1 @@ +You have {0} shots. diff --git a/77_Salvo/csharp/Resources/YouHit.txt b/77_Salvo/csharp/Resources/YouHit.txt new file mode 100644 index 00000000..b5b5d761 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHit.txt @@ -0,0 +1 @@ +You hit my {0}. diff --git a/77_Salvo/csharp/Resources/YouWon.txt b/77_Salvo/csharp/Resources/YouWon.txt new file mode 100644 index 00000000..1f71343b --- /dev/null +++ b/77_Salvo/csharp/Resources/YouWon.txt @@ -0,0 +1 @@ +You have won. diff --git a/77_Salvo/csharp/Salvo.csproj b/77_Salvo/csharp/Salvo.csproj index d3fe4757..51470ec9 100644 --- a/77_Salvo/csharp/Salvo.csproj +++ b/77_Salvo/csharp/Salvo.csproj @@ -2,8 +2,16 @@ Exe net6.0 - 10 + 11 enable enable + + + + + + + + diff --git a/77_Salvo/csharp/Ships/Battleship.cs b/77_Salvo/csharp/Ships/Battleship.cs new file mode 100644 index 00000000..4f864eb6 --- /dev/null +++ b/77_Salvo/csharp/Ships/Battleship.cs @@ -0,0 +1,17 @@ +namespace Salvo.Ships; + +internal sealed class Battleship : Ship +{ + internal Battleship(IReadWrite io) + : base(io) + { + } + + internal Battleship(IRandom random) + : base(random) + { + } + + internal override int Shots => 3; + internal override int Size => 5; +} diff --git a/77_Salvo/csharp/Ships/Cruiser.cs b/77_Salvo/csharp/Ships/Cruiser.cs new file mode 100644 index 00000000..d004f24c --- /dev/null +++ b/77_Salvo/csharp/Ships/Cruiser.cs @@ -0,0 +1,17 @@ +namespace Salvo.Ships; + +internal sealed class Cruiser : Ship +{ + internal Cruiser(IReadWrite io) + : base(io) + { + } + + internal Cruiser(IRandom random) + : base(random) + { + } + + internal override int Shots => 2; + internal override int Size => 3; +} diff --git a/77_Salvo/csharp/Ships/Destroyer.cs b/77_Salvo/csharp/Ships/Destroyer.cs new file mode 100644 index 00000000..6523395d --- /dev/null +++ b/77_Salvo/csharp/Ships/Destroyer.cs @@ -0,0 +1,17 @@ +namespace Salvo.Ships; + +internal sealed class Destroyer : Ship +{ + internal Destroyer(string nameIndex, IReadWrite io) + : base(io, $"<{nameIndex}>") + { + } + + internal Destroyer(string nameIndex, IRandom random) + : base(random, $"<{nameIndex}>") + { + } + + internal override int Shots => 1; + internal override int Size => 2; +} diff --git a/77_Salvo/csharp/Ships/Ship.cs b/77_Salvo/csharp/Ships/Ship.cs new file mode 100644 index 00000000..ee204d3f --- /dev/null +++ b/77_Salvo/csharp/Ships/Ship.cs @@ -0,0 +1,37 @@ +namespace Salvo.Ships; + +internal abstract class Ship +{ + private readonly List _positions = new(); + + protected Ship(IReadWrite io, string? nameSuffix = null) + { + Name = GetType().Name + nameSuffix; + _positions = io.ReadPositions(Name, Size).ToList(); + } + + protected Ship(IRandom random, string? nameSuffix = null) + { + Name = GetType().Name + nameSuffix; + + var (start, delta) = random.GetRandomShipPositionInRange(Size); + for (var i = 0; i < Size; i++) + { + _positions.Add(start + delta * i); + } + } + + internal string Name { get; } + internal abstract int Shots { get; } + internal abstract int Size { get; } + internal bool IsDamaged => _positions.Count > 0 && _positions.Count < Size; + internal bool IsDestroyed => _positions.Count == 0; + + internal bool IsHit(Position position) => _positions.Remove(position); + + internal float DistanceTo(Ship other) + => _positions.SelectMany(a => other._positions.Select(b => a.DistanceTo(b))).Min(); + + public override string ToString() + => string.Join(Environment.NewLine, _positions.Select(p => p.ToString()).Prepend(Name)); +} diff --git a/77_Salvo/csharp/Targetting/ComputerShotSelector.cs b/77_Salvo/csharp/Targetting/ComputerShotSelector.cs new file mode 100644 index 00000000..721f50c5 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ComputerShotSelector.cs @@ -0,0 +1,33 @@ +namespace Salvo.Targetting; + +internal class ComputerShotSelector : ShotSelector +{ + private readonly KnownHitsShotSelectionStrategy _knownHitsStrategy; + private readonly SearchPatternShotSelectionStrategy _searchPatternStrategy; + private readonly IReadWrite _io; + private readonly bool _showShots; + + internal ComputerShotSelector(Fleet source, IRandom random, IReadWrite io) + : base(source) + { + _knownHitsStrategy = new KnownHitsShotSelectionStrategy(this); + _searchPatternStrategy = new SearchPatternShotSelectionStrategy(this, random); + _io = io; + _showShots = io.ReadString(Prompts.SeeShots).Equals("yes", StringComparison.InvariantCultureIgnoreCase); + } + + protected override IEnumerable GetShots() + { + var shots = GetSelectionStrategy().GetShots(NumberOfShots).ToArray(); + if (_showShots) + { + _io.WriteLine(string.Join(Environment.NewLine, shots)); + } + return shots; + } + + internal void RecordHit(Ship ship, int turn) => _knownHitsStrategy.RecordHit(ship, turn); + + private ShotSelectionStrategy GetSelectionStrategy() + => _knownHitsStrategy.KnowsOfDamagedShips ? _knownHitsStrategy : _searchPatternStrategy; +} diff --git a/77_Salvo/csharp/Targetting/HumanShotSelector.cs b/77_Salvo/csharp/Targetting/HumanShotSelector.cs new file mode 100644 index 00000000..ea49bf12 --- /dev/null +++ b/77_Salvo/csharp/Targetting/HumanShotSelector.cs @@ -0,0 +1,34 @@ +namespace Salvo.Targetting; + +internal class HumanShotSelector : ShotSelector +{ + private readonly IReadWrite _io; + + internal HumanShotSelector(Fleet source, IReadWrite io) + : base(source) + { + _io = io; + } + + protected override IEnumerable GetShots() + { + var shots = new Position[NumberOfShots]; + + for (var i = 0; i < shots.Length; i++) + { + while (true) + { + var position = _io.ReadValidPosition(); + if (WasSelectedPreviously(position, out var turnTargeted)) + { + _io.WriteLine($"YOU SHOT THERE BEFORE ON TURN {turnTargeted}"); + continue; + } + shots[i] = position; + break; + } + } + + return shots; + } +} diff --git a/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs b/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs new file mode 100644 index 00000000..5e0b9d2c --- /dev/null +++ b/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs @@ -0,0 +1,71 @@ +namespace Salvo.Targetting; + +internal class KnownHitsShotSelectionStrategy : ShotSelectionStrategy +{ + private readonly List<(int Turn, Ship Ship)> _damagedShips = new(); + + internal KnownHitsShotSelectionStrategy(ShotSelector shotSelector) + : base(shotSelector) + { + } + + internal bool KnowsOfDamagedShips => _damagedShips.Any(); + + internal override IEnumerable GetShots(int numberOfShots) + { + var tempGrid = Position.All.ToDictionary(x => x, _ => 0); + var shots = Enumerable.Range(1, numberOfShots).Select(x => new Position(x, x)).ToArray(); + + foreach (var (hitTurn, ship) in _damagedShips) + { + foreach (var position in Position.All) + { + if (WasSelectedPreviously(position)) + { + tempGrid[position]=-10000000; + continue; + } + + foreach (var neighbour in position.Neighbours) + { + if (WasSelectedPreviously(neighbour, out var turn) && turn == hitTurn) + { + tempGrid[position] += hitTurn + 10 - position.Y * ship.Shots; + } + } + } + } + + foreach (var position in Position.All) + { + var Q9=0; + for (var i = 0; i < numberOfShots; i++) + { + if (tempGrid[shots[i]] < tempGrid[shots[Q9]]) + { + Q9 = i; + } + } + if (position.X <= numberOfShots && position.IsOnDiagonal) { continue; } + if (tempGrid[position] x.Ship == ship); + } + else + { + _damagedShips.Add((turn, ship)); + } + } +} diff --git a/77_Salvo/csharp/Targetting/SearchPattern.cs b/77_Salvo/csharp/Targetting/SearchPattern.cs new file mode 100644 index 00000000..ac2071c8 --- /dev/null +++ b/77_Salvo/csharp/Targetting/SearchPattern.cs @@ -0,0 +1,22 @@ +using System.Collections.Immutable; + +namespace Salvo.Targetting; + +internal class SearchPattern +{ + private static readonly ImmutableArray _offsets = + ImmutableArray.Create(new(1, 1), new(-1, 1), new(1, -3), new(1, 1), new(0, 2), new(-1, 1)); + + private int _nextIndex; + + internal bool TryGetOffset(out Offset offset) + { + offset = default; + if (_nextIndex >= _offsets.Length) { return false; } + + offset = _offsets[_nextIndex++]; + return true; + } + + internal void Reset() => _nextIndex = 0; +} \ No newline at end of file diff --git a/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs b/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs new file mode 100644 index 00000000..1eae88f8 --- /dev/null +++ b/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs @@ -0,0 +1,55 @@ +namespace Salvo.Targetting; + +internal class SearchPatternShotSelectionStrategy : ShotSelectionStrategy +{ + private const int MaxSearchPatternAttempts = 100; + private readonly IRandom _random; + private readonly SearchPattern _searchPattern = new(); + private readonly List _shots = new(); + + internal SearchPatternShotSelectionStrategy(ShotSelector shotSelector, IRandom random) + : base(shotSelector) + { + _random = random; + } + + internal override IEnumerable GetShots(int numberOfShots) + { + _shots.Clear(); + while(_shots.Count < numberOfShots) + { + var (seed, _) = _random.NextShipPosition(); + SearchFrom(numberOfShots, seed); + } + return _shots; + } + + private void SearchFrom(int numberOfShots, Position candidateShot) + { + var attemptsLeft = MaxSearchPatternAttempts; + while (true) + { + _searchPattern.Reset(); + if (attemptsLeft-- == 0) { return; } + candidateShot = candidateShot.BringIntoRange(_random); + if (FindValidShots(numberOfShots, ref candidateShot)) { return; } + } + } + + private bool FindValidShots(int numberOfShots, ref Position candidateShot) + { + while (true) + { + if (IsValidShot(candidateShot)) + { + _shots.Add(candidateShot); + if (_shots.Count == numberOfShots) { return true; } + } + if (!_searchPattern.TryGetOffset(out var offset)) { return false; } + candidateShot += offset; + } + } + + private bool IsValidShot(Position candidate) + => candidate.IsInRange && !WasSelectedPreviously(candidate) && !_shots.Contains(candidate); +} \ No newline at end of file diff --git a/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs b/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs new file mode 100644 index 00000000..b4cf14b1 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs @@ -0,0 +1,17 @@ +namespace Salvo.Targetting; + +internal abstract class ShotSelectionStrategy +{ + private readonly ShotSelector _shotSelector; + protected ShotSelectionStrategy(ShotSelector shotSelector) + { + _shotSelector = shotSelector; + } + + internal abstract IEnumerable GetShots(int numberOfShots); + + protected bool WasSelectedPreviously(Position position) => _shotSelector.WasSelectedPreviously(position); + + protected bool WasSelectedPreviously(Position position, out int turn) + => _shotSelector.WasSelectedPreviously(position, out turn); +} diff --git a/77_Salvo/csharp/Targetting/ShotSelector.cs b/77_Salvo/csharp/Targetting/ShotSelector.cs new file mode 100644 index 00000000..695dbc42 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ShotSelector.cs @@ -0,0 +1,31 @@ +namespace Salvo.Targetting; + +internal abstract class ShotSelector +{ + private readonly Fleet _source; + private readonly Dictionary _previousShots = new(); + + internal ShotSelector(Fleet source) + { + _source = source; + } + + internal int NumberOfShots => _source.Ships.Sum(s => s.Shots); + internal bool CanTargetAllRemainingSquares => NumberOfShots >= 100 - _previousShots.Count; + + internal bool WasSelectedPreviously(Position position) => _previousShots.ContainsKey(position); + + internal bool WasSelectedPreviously(Position position, out int turn) + => _previousShots.TryGetValue(position, out turn); + + internal IEnumerable GetShots(int turnNumber) + { + foreach (var shot in GetShots()) + { + _previousShots.Add(shot, turnNumber); + yield return shot; + } + } + + protected abstract IEnumerable GetShots(); +} diff --git a/77_Salvo/csharp/TurnHandler.cs b/77_Salvo/csharp/TurnHandler.cs new file mode 100644 index 00000000..afde9613 --- /dev/null +++ b/77_Salvo/csharp/TurnHandler.cs @@ -0,0 +1,92 @@ +using Salvo.Targetting; + +namespace Salvo; + +internal class TurnHandler +{ + private readonly IReadWrite _io; + private readonly Fleet _humanFleet; + private readonly Fleet _computerFleet; + private readonly bool _humanStarts; + private readonly HumanShotSelector _humanShotSelector; + private readonly ComputerShotSelector _computerShotSelector; + private readonly Func _turnAction; + private int _turnNumber; + + public TurnHandler(IReadWrite io, IRandom random) + { + _io = io; + _computerFleet = new Fleet(random); + _humanFleet = new Fleet(io); + _turnAction = AskWhoStarts() + ? () => PlayHumanTurn() ?? PlayComputerTurn() + : () => PlayComputerTurn() ?? PlayHumanTurn(); + _humanShotSelector = new HumanShotSelector(_humanFleet, io); + _computerShotSelector = new ComputerShotSelector(_computerFleet, random, io); + } + + public Winner? PlayTurn() + { + _io.Write(Strings.Turn(++_turnNumber)); + return _turnAction.Invoke(); + } + + private bool AskWhoStarts() + { + while (true) + { + var startResponse = _io.ReadString(Prompts.Start); + if (startResponse.Equals(Strings.WhereAreYourShips, StringComparison.InvariantCultureIgnoreCase)) + { + foreach (var ship in _computerFleet.Ships) + { + _io.WriteLine(ship); + } + } + else + { + return startResponse.Equals("yes", StringComparison.InvariantCultureIgnoreCase); + } + } + } + + private Winner? PlayComputerTurn() + { + var numberOfShots = _computerShotSelector.NumberOfShots; + _io.Write(Strings.IHaveShots(numberOfShots)); + if (numberOfShots == 0) { return Winner.Human; } + if (_computerShotSelector.CanTargetAllRemainingSquares) + { + _io.Write(Streams.IHaveMoreShotsThanSquares); + return Winner.Computer; + } + + _humanFleet.ReceiveShots( + _computerShotSelector.GetShots(_turnNumber), + ship => + { + _io.Write(Strings.IHit(ship.Name)); + _computerShotSelector.RecordHit(ship, _turnNumber); + }); + + return null; + } + + private Winner? PlayHumanTurn() + { + var numberOfShots = _humanShotSelector.NumberOfShots; + _io.Write(Strings.YouHaveShots(numberOfShots)); + if (numberOfShots == 0) { return Winner.Computer; } + if (_humanShotSelector.CanTargetAllRemainingSquares) + { + _io.WriteLine(Streams.YouHaveMoreShotsThanSquares); + return Winner.Human; + } + + _computerFleet.ReceiveShots( + _humanShotSelector.GetShots(_turnNumber), + ship => _io.Write(Strings.YouHit(ship.Name))); + + return null; + } +} diff --git a/77_Salvo/csharp/Winner.cs b/77_Salvo/csharp/Winner.cs new file mode 100644 index 00000000..a5a73eb4 --- /dev/null +++ b/77_Salvo/csharp/Winner.cs @@ -0,0 +1,7 @@ +namespace Salvo; + +internal enum Winner +{ + Human, + Computer +} diff --git a/84_Super_Star_Trek/python/superstartrek.py b/84_Super_Star_Trek/python/superstartrek.py index 21e03e45..2a86efba 100644 --- a/84_Super_Star_Trek/python/superstartrek.py +++ b/84_Super_Star_Trek/python/superstartrek.py @@ -8,7 +8,6 @@ **** LEEDOM - APRIL & DECEMBER 1974, **** WITH A LITTLE HELP FROM HIS FRIENDS . . . -Python translation by Jack Boyce - February 2021 Output is identical to BASIC version except for a few fixes (as noted, search `bug`) and minor cleanup. """ diff --git a/84_Super_Star_Trek/rust/Cargo.toml b/84_Super_Star_Trek/rust/Cargo.toml new file mode 100644 index 00000000..01457249 --- /dev/null +++ b/84_Super_Star_Trek/rust/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ctrlc = "3.2.5" +rand = "0.8.5" diff --git a/84_Super_Star_Trek/rust/readme.md b/84_Super_Star_Trek/rust/readme.md new file mode 100644 index 00000000..ae2f58dc --- /dev/null +++ b/84_Super_Star_Trek/rust/readme.md @@ -0,0 +1,19 @@ +# Super Star Trek - Rust version + +Explanation of modules: + +- [main.rs](./src/main.rs) - creates the galaxy (generation functions are in model.rs as impl methods) then loops listening for commands. after each command checks for victory or defeat condtions. +- [model.rs](./src/model.rs) - all the structs and enums that represent the galaxy. key methods in here (as impl methods) are generation functions on galaxy and quadrant, and various comparison methods on the 'Pos' tuple type. +- [commands.rs](./src/commands.rs) - most of the code that implements instructions given by the player (some code logic is in the model impls, and some in view.rs if its view only). +- [view.rs](./src/view.rs) - all text printed to the output, mostly called by command.rs (like view::bad_nav for example). also contains the prompts printed to the user (e.g. view::prompts::COMMAND). +- [input.rs](./src/input.rs) - utility methods for getting input from the user, including logic for parsing numbers, repeating prompts until a correct value is provided etc. + +Basically the user is asked for the next command, this runs a function that usually checks if the command system is working, and if so will gather additional input (see next note for a slight change here), then either the model is read and info printed, or its mutated in some way (e.g. firing a torpedo, which reduces the torpedo count on the enterprise and can destroy klingons and star bases; finally the klingons fire back and can destroy the enterprise). Finally the win/lose conditions are checked before the loop repeats. + +## Changes from the original + +I have tried to keep it as close as possible. Notable changes are: + +- commands can be given with parameters in line. e.g. while 'nav' will ask for course and then warp speed in the original, here you can *optionally* also do this as one line, e.g. `nav 1 0.1` to move one sector east. I'm sorry - it was driving me insane in its original form (which is still sorted, as is partial application e.g. nav 1 to preset direction and then provide speed). +- text is mostly not uppercase, as text was in the basic version. this would be easy to change however as all text is in view.rs, but I chose not to. +- the navigation system (plotting direction, paths and collision detection) is as close as I could make it to the basic version (by using other language conversions as specification sources) but I suspect is not perfect. seems to work well enough however. \ No newline at end of file diff --git a/84_Super_Star_Trek/rust/src/commands.rs b/84_Super_Star_Trek/rust/src/commands.rs new file mode 100644 index 00000000..c1c4bc8d --- /dev/null +++ b/84_Super_Star_Trek/rust/src/commands.rs @@ -0,0 +1,543 @@ +use rand::Rng; + +use crate::{model::*, view, input::{self, param_or_prompt_value, prompt_two_values}}; + +pub fn perform_short_range_scan(galaxy: &Galaxy) { + if galaxy.enterprise.damaged.contains_key(systems::SHORT_RANGE_SCAN) { + view::scanners_out(); + return; + } + + view::short_range_scan(&galaxy) +} + +pub fn get_amount_and_set_shields(galaxy: &mut Galaxy, provided: Vec) { + + if galaxy.enterprise.damaged.contains_key(systems::SHIELD_CONTROL) { + view::inoperable(&systems::name_for(systems::SHIELD_CONTROL)); + return; + } + + view::energy_available(galaxy.enterprise.total_energy); + let value = input::param_or_prompt_value(&provided, 0, view::prompts::SHIELDS, 0, i32::MAX); + if value.is_none() { + view::shields_unchanged(); + return; + } + + let value = value.unwrap() as u16; + if value > galaxy.enterprise.total_energy { + view::ridiculous(); + view::shields_unchanged(); + return; + } + + galaxy.enterprise.shields = value; + view::shields_set(value); +} + +pub fn gather_dir_and_speed_then_move(galaxy: &mut Galaxy, provided: Vec) { + + let course = input::param_or_prompt_value(&provided, 0, view::prompts::COURSE, 1.0, 9.0); + if course.is_none() { + view::bad_course_data(); + return; + } + + let course = course.unwrap(); + + let mut max_warp = 8.0; + if galaxy.enterprise.damaged.contains_key(systems::WARP_ENGINES) { + max_warp = 0.2; + } + + let speed = input::param_or_prompt_value(&provided, 1, &view::prompts::warp_factor(max_warp), 0.0, 8.0); + if speed.is_none() { + view::bad_course_data(); + return; + } + + let speed = speed.unwrap(); + + if speed > max_warp { + view::damaged_engines(max_warp, speed); + return; + } + + klingons_move(galaxy); + klingons_fire(galaxy); + + if galaxy.enterprise.destroyed { + return; + } + + repair_systems(&mut galaxy.enterprise, speed); + repair_or_damage_random_system(&mut galaxy.enterprise); + + move_enterprise(course, speed, galaxy); +} + +fn repair_systems(enterprise: &mut Enterprise, amount: f32) { + + let keys: Vec = enterprise.damaged.keys().map(|k| k.to_string()).collect(); + let mut repaired = Vec::new(); + for key in keys { + let fully_fixed = enterprise.repair_system(&key, amount); + if fully_fixed { + repaired.push(systems::name_for(&key)); + } + } + + if repaired.len() <= 0 { + return; + } + + view::damage_control_report(); + for name in repaired { + view::system_repair_completed(name); + } +} + +fn repair_or_damage_random_system(enterprise: &mut Enterprise) { + let mut rng = rand::thread_rng(); + + if rng.gen::() > 0.2 { + return; + } + + if rng.gen::() >= 0.6 { + if enterprise.damaged.len() == 0 { + return; + } + + let damaged: Vec = enterprise.damaged.keys().map(|k| k.to_string()).collect(); + let system = damaged[rng.gen_range(0..damaged.len())].to_string(); + let system_name = &systems::name_for(&system); + + enterprise.repair_system(&system, rng.gen::() * 3.0 + 1.0); + view::random_repair_report_for(system_name, false); + return; + } + + let system = systems::KEYS[rng.gen_range(0..systems::KEYS.len())].to_string(); + let system_name = &systems::name_for(&system); + + enterprise.damage_system(&system, rng.gen::() * 5.0 + 1.0); + view::random_repair_report_for(system_name, true); +} + +fn move_enterprise(course: f32, warp_speed: f32, galaxy: &mut Galaxy) { + + let ship = &mut galaxy.enterprise; + + let (mut path, mut hit_edge) = find_nav_path(ship.quadrant, ship.sector, course, warp_speed); + for i in 0..path.len() { + let (quadrant, sector) = path[i].to_local_quadrant_sector(); + if quadrant != ship.quadrant { + break; // have left current quadrant, so collision checks removed. if there is a collision at the dest... /shrug? + } + let quadrant = &galaxy.quadrants[quadrant.as_index()]; + if quadrant.sector_status(sector) != SectorStatus::Empty { + path = path[..i].into(); + hit_edge = false; + if i > 0 { + let (_, last_sector) = path[path.len() - 1].to_local_quadrant_sector(); + view::bad_nav(last_sector); + } else { + view::bad_nav(ship.sector); + return; + } + break; + } + } + + if path.len() == 0 { + if hit_edge { + view::hit_edge(ship.quadrant, ship.sector); + } + return; + } + + let energy_cost = path.len() as u16 + 10; + + if energy_cost > ship.total_energy { + view::insuffient_warp_energy(warp_speed); + return + } + + let (end_quadrant, end_sector) = path[path.len() - 1].to_local_quadrant_sector(); + if hit_edge { + view::hit_edge(end_quadrant, end_sector); + } + + if ship.quadrant != end_quadrant { + view::enter_quadrant(end_quadrant); + galaxy.scanned.insert(end_quadrant); + + if galaxy.quadrants[end_quadrant.as_index()].klingons.len() > 0 { + view::condition_red(); + if ship.shields <= 200 { + view::danger_shields(); + } + } + } + + ship.quadrant = end_quadrant; + ship.sector = end_sector; + + let quadrant = &galaxy.quadrants[end_quadrant.as_index()]; + if quadrant.docked_at_starbase(ship.sector) { + ship.shields = 0; + ship.photon_torpedoes = MAX_PHOTON_TORPEDOES; + ship.total_energy = MAX_ENERGY; + } else { + ship.total_energy = ship.total_energy - energy_cost; + if ship.shields > ship.total_energy { + view::divert_energy_from_shields(); + ship.shields = ship.total_energy; + } + } + + view::short_range_scan(&galaxy) +} + +fn find_nav_path(start_quadrant: Pos, start_sector: Pos, course: f32, warp_speed: f32) -> (Vec, bool) { + + let (dx, dy) = calculate_delta(course); + + let mut distance = (warp_speed * 8.0) as i8; + if distance == 0 { + distance = 1; + } + + let mut last_sector = start_sector.as_galactic_sector(start_quadrant); + let mut path = Vec::new(); + let mut hit_edge; + + loop { + let nx = (last_sector.0 as f32 + dx) as i8; + let ny = (last_sector.1 as f32 + dy) as i8; + hit_edge = nx < 0 || ny < 0 || nx >= 64 || ny >= 64; + if hit_edge { + break; + } + last_sector = Pos(nx as u8, ny as u8); + path.push(last_sector); + + distance -= 1; + if distance == 0 { + break; + } + } + + (path, hit_edge) +} + +fn calculate_delta(course: f32) -> (f32, f32) { + // this course delta stuff is a translation (of a translation, of a translation...) of the original basic calcs + let dir = (course - 1.0) % 8.0; + let (dx1, dy1) = COURSES[dir as usize]; + let (dx2, dy2) = COURSES[(dir + 1.0) as usize]; + let frac = dir - (dir as i32) as f32; + + let dx = dx1 + (dx2 - dx1) * frac; + let dy = dy1 + (dy2 - dy1) * frac; + + (dx, dy) +} + +fn klingons_move(galaxy: &mut Galaxy) { + let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()]; + for k in 0..quadrant.klingons.len() { + let new_sector: Pos; + loop { + let candidate = quadrant.find_empty_sector(); + if candidate != galaxy.enterprise.sector { + new_sector = candidate; + break; + } + } + quadrant.klingons[k].sector = new_sector; + } +} + +fn klingons_fire(galaxy: &mut Galaxy) { + let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()]; + if quadrant.docked_at_starbase(galaxy.enterprise.sector) { + view::starbase_shields(); + return; + } + + for k in 0..quadrant.klingons.len() { + quadrant.klingons[k].fire_on(&mut galaxy.enterprise); + } +} + +pub fn run_damage_control(galaxy: &Galaxy) { + if galaxy.enterprise.damaged.contains_key(systems::DAMAGE_CONTROL) { + view::inoperable(&systems::name_for(systems::DAMAGE_CONTROL)); + return; + } + + view::damage_control(&galaxy.enterprise); +} + +pub fn try_starbase_ship_repair(galaxy: &mut Galaxy) { + let ship = &mut galaxy.enterprise; + let quadrant = &galaxy.quadrants[ship.quadrant.as_index()]; + if ship.damaged.len() == 0 || !quadrant.docked_at_starbase(ship.sector) { + return; + } + + let repair_delay = quadrant.star_base.as_ref().unwrap().repair_delay; + let repair_time = (ship.damaged.len() as f32 * 0.1 + repair_delay).max(0.9); + + view::repair_estimate(repair_time); + if !input::prompt_yes_no(view::prompts::REPAIR) { + return; + } + + ship.damaged.clear(); + galaxy.stardate += repair_time; + view::damage_control(&ship); +} + +pub fn perform_long_range_scan(galaxy: &mut Galaxy) { + if galaxy.enterprise.damaged.contains_key(systems::LONG_RANGE_SCAN) { + view::inoperable(&systems::name_for(systems::LONG_RANGE_SCAN)); + return; + } + + let seen = view::long_range_scan(galaxy); + for pos in seen { + galaxy.scanned.insert(pos); + } +} + +pub fn access_computer(galaxy: &Galaxy, provided: Vec) { + if galaxy.enterprise.damaged.contains_key(systems::COMPUTER) { + view::inoperable(&systems::name_for(systems::COMPUTER)); + return; + } + + let operation : i32; + loop { + let entered = input::param_or_prompt_value(&provided, 0, view::prompts::COMPUTER, 0, 5); + if entered.is_none() { + view::computer_options(); + } else { + operation = entered.unwrap(); + break; + } + } + + match operation { + 0 => view::galaxy_scanned_map(galaxy), + 1 => { + view::status_report(galaxy); + run_damage_control(galaxy); + }, + 2 => show_klingon_direction_data(galaxy), + 3 => show_starbase_direction_data(galaxy), + 4 => direction_dist_calculator(galaxy), + 5 => view::galaxy_region_map(), + _ => () // unreachable + } +} + +fn show_klingon_direction_data(galaxy: &Galaxy) { + let quadrant = &galaxy.quadrants[galaxy.enterprise.quadrant.as_index()]; + if quadrant.klingons.len() == 0 { + view::no_local_enemies(); + return; + } + + view::klingon_report(quadrant.klingons.len() > 1); + let origin = galaxy.enterprise.sector; + for k in &quadrant.klingons { + let target = k.sector; + view::direction_distance(origin.direction(target), origin.dist(target)) + } +} + +fn show_starbase_direction_data(galaxy: &Galaxy) { + let quadrant = &galaxy.quadrants[galaxy.enterprise.quadrant.as_index()]; + match &quadrant.star_base { + None => { + view::no_local_starbase(); + return; + }, + Some(s) => { + view::starbase_report(); + let origin = galaxy.enterprise.sector; + let target = s.sector; + view::direction_distance(origin.direction(target), origin.dist(target)) + } + } +} + +fn direction_dist_calculator(galaxy: &Galaxy) { + view::direction_dist_intro(&galaxy.enterprise); + loop { + let coords1 = prompt_two_values(view::prompts::INITIAL_COORDS, 1, 8).map(|(x, y)| Pos(x, y)); + if coords1.is_none() { + continue; + } + let coords2 = prompt_two_values(view::prompts::TARGET_COORDS, 1, 8).map(|(x, y)| Pos(x, y)); + if coords2.is_none() { + continue; + } + let dir = coords1.unwrap().direction(coords2.unwrap()); + let dist = coords1.unwrap().dist(coords2.unwrap()); + view::direction_distance(dir, dist); + break; + } +} + +pub fn get_power_and_fire_phasers(galaxy: &mut Galaxy, provided: Vec) { + if galaxy.enterprise.damaged.contains_key(systems::PHASERS) { + view::inoperable(&systems::name_for(systems::PHASERS)); + return; + } + + let quadrant = &mut galaxy.quadrants[galaxy.enterprise.quadrant.as_index()]; + if quadrant.klingons.len() == 0 { + view::no_local_enemies(); + return; + } + + let computer_damaged = galaxy.enterprise.damaged.contains_key(systems::COMPUTER); + if computer_damaged { + view::computer_accuracy_issue(); + } + + let available_energy = galaxy.enterprise.total_energy - galaxy.enterprise.shields; + view::phasers_locked(available_energy); + let mut power: f32; + loop { + let setting = param_or_prompt_value(&provided, 0, view::prompts::PHASERS, 0, available_energy); + if setting.is_some() { + power = setting.unwrap() as f32; + break; + } + } + + if power == 0.0 { + return; + } + + galaxy.enterprise.total_energy -= power as u16; + + let mut rng = rand::thread_rng(); + if computer_damaged { + power *= rng.gen::(); + } + + let per_enemy = power / quadrant.klingons.len() as f32; + + for k in &mut quadrant.klingons { + let dist = k.sector.abs_diff(galaxy.enterprise.sector) as f32; + let hit_strength = per_enemy / dist * (2.0 + rng.gen::()); + if hit_strength < 0.15 * k.energy { + view::no_damage(k.sector); + } else { + k.energy -= hit_strength; + view::hit_on_klingon(hit_strength, k.sector); + if k.energy > 0.0 { + view::klingon_remaining_energy(k.energy); + } else { + view::klingon_destroyed(); + } + } + } + + quadrant.klingons.retain(|k| k.energy > 0.0); + + klingons_fire(galaxy); +} + +pub fn gather_dir_and_launch_torpedo(galaxy: &mut Galaxy, provided: Vec) { + let star_bases = galaxy.remaining_starbases(); + let ship = &mut galaxy.enterprise; + + if ship.damaged.contains_key(systems::TORPEDOES) { + view::inoperable(&systems::name_for(systems::TORPEDOES)); + return; + } + + if ship.photon_torpedoes == 0 { + view::no_torpedoes_remaining(); + return; + } + + let course = input::param_or_prompt_value(&provided, 0, view::prompts::TORPEDO_COURSE, 1.0, 9.0); + if course.is_none() { + view::bad_torpedo_course(); + return; + } + + ship.photon_torpedoes -= 1; + view::torpedo_track(); + + let path = find_torpedo_path(ship.sector, course.unwrap()); + let quadrant = &mut galaxy.quadrants[ship.quadrant.as_index()]; + let mut hit = false; + for p in path { + view::torpedo_path(p); + match quadrant.sector_status(p) { + SectorStatus::Empty => continue, + SectorStatus::Star => { + hit = true; + view::star_absorbed_torpedo(p); + break; + }, + SectorStatus::Klingon => { + hit = true; + quadrant.get_klingon(p).unwrap().energy = 0.0; + quadrant.klingons.retain(|k| k.energy > 0.0); + view::klingon_destroyed(); + break; + }, + SectorStatus::StarBase => { + hit = true; + quadrant.star_base = None; + let remaining = star_bases - 1; + view::destroyed_starbase(remaining > 0); + if remaining == 0 { + ship.destroyed = true; + } + break; + } + } + } + + if ship.destroyed { // if you wiped out the last starbase, trigger game over + return; + } + + if !hit { + view::torpedo_missed(); + } + + klingons_fire(galaxy); +} + +fn find_torpedo_path(start_sector: Pos, course: f32) -> Vec { + + let (dx, dy) = calculate_delta(course); + + let mut last_sector = start_sector; + let mut path = Vec::new(); + + loop { + let nx = (last_sector.0 as f32 + dx) as i8; + let ny = (last_sector.1 as f32 + dy) as i8; + if nx < 0 || ny < 0 || nx >= 8 || ny >= 8 { + break; + } + last_sector = Pos(nx as u8, ny as u8); + path.push(last_sector); + } + + path +} \ No newline at end of file diff --git a/84_Super_Star_Trek/rust/src/input.rs b/84_Super_Star_Trek/rust/src/input.rs new file mode 100644 index 00000000..62d4daee --- /dev/null +++ b/84_Super_Star_Trek/rust/src/input.rs @@ -0,0 +1,73 @@ +use std::{io::{stdin, stdout, Write}, str::FromStr}; + +pub fn prompt(prompt_text: &str) -> Vec { + let stdin = stdin(); + let mut stdout = stdout(); + + print!("{prompt_text} "); + let _ = stdout.flush(); + + let mut buffer = String::new(); + if let Ok(_) = stdin.read_line(&mut buffer) { + return buffer.trim_end().split([' ', ',']).map(|s| s.to_string()).collect(); + } + Vec::new() +} + +pub fn prompt_yes_no(prompt_text: &str) -> bool { + loop { + let response = prompt(&format!("{prompt_text} (Y/N)")); + if response.len() == 0 { + continue; + } + let first_word = response[0].to_uppercase(); + if first_word.starts_with("Y") { + return true; + } + if first_word.starts_with("N") { + return false; + } + } +} + +pub fn prompt_value(prompt_text: &str, min: T, max: T) -> Option { + let passed = prompt(prompt_text); + if passed.len() != 1 { + return None + } + match passed[0].parse::() { + Ok(n) if (n >= min && n <= max) => Some(n), + _ => None + } +} + +pub fn param_or_prompt_value(params: &Vec, param_pos: usize, prompt_text: &str, min: T, max: T) -> Option { + let mut res: Option = None; + if params.len() > param_pos { + match params[param_pos].parse::() { + Ok(n) if (n >= min && n <= max) => res = Some(n), + _ => () + } + } + if res.is_some() { + return res; + } + return prompt_value::(prompt_text, min, max); +} + +pub fn prompt_two_values(prompt_text: &str, min: T, max: T) -> Option<(T, T)> { + let passed = prompt(prompt_text); + if passed.len() != 2 { + return None + } + match passed[0].parse::() { + Ok(n1) if (n1 >= min && n1 <= max) => { + match passed[1].parse::() { + Ok(n2) if (n2 >= min && n2 <= max) => + Some((n1, n2)), + _ => None + } + } + _ => None + } +} \ No newline at end of file diff --git a/84_Super_Star_Trek/rust/src/main.rs b/84_Super_Star_Trek/rust/src/main.rs new file mode 100644 index 00000000..4c1a277e --- /dev/null +++ b/84_Super_Star_Trek/rust/src/main.rs @@ -0,0 +1,70 @@ +use std::process::exit; + +use input::{prompt, prompt_yes_no}; +use model::{Galaxy, systems}; + +mod input; +mod model; +mod commands; +mod view; + +fn main() { + ctrlc::set_handler(move || { exit(0) }) + .expect("Error setting Ctrl-C handler"); + + view::title(); + if prompt_yes_no(view::prompts::INSTRUCTIONS) { + view::full_instructions(); + let _ = input::prompt(view::prompts::WHEN_READY); + } + + let mut galaxy = Galaxy::generate_new(); + let initial_klingons = galaxy.remaining_klingons(); + let initial_stardate = galaxy.stardate; + + view::enterprise(); + view::intro(&galaxy); + let _ = input::prompt(view::prompts::WHEN_READY); + + view::starting_quadrant(galaxy.enterprise.quadrant); + view::short_range_scan(&galaxy); + + loop { + let command = input::prompt(view::prompts::COMMAND); + if command.len() == 0 { + continue; + } + match command[0].to_uppercase().as_str() { // order is weird because i built it in this order :) + systems::SHORT_RANGE_SCAN => commands::perform_short_range_scan(&galaxy), + systems::WARP_ENGINES => commands::gather_dir_and_speed_then_move(&mut galaxy, command[1..].into()), + systems::SHIELD_CONTROL => commands::get_amount_and_set_shields(&mut galaxy, command[1..].into()), + systems::DAMAGE_CONTROL => { + commands::run_damage_control(&galaxy); + commands::try_starbase_ship_repair(&mut galaxy); + } + systems::LONG_RANGE_SCAN => commands::perform_long_range_scan(&mut galaxy), + systems::COMPUTER => commands::access_computer(&galaxy, command[1..].into()), + systems::PHASERS => commands::get_power_and_fire_phasers(&mut galaxy, command[1..].into()), + systems::TORPEDOES => commands::gather_dir_and_launch_torpedo(&mut galaxy, command[1..].into()), + systems::RESIGN => galaxy.enterprise.destroyed = true, + _ => view::print_command_help() + } + + if galaxy.enterprise.destroyed || galaxy.enterprise.is_stranded() || galaxy.stardate >= galaxy.final_stardate { + view::end_game_failure(&galaxy); + if galaxy.remaining_klingons() > 0 && galaxy.remaining_starbases() > 0 && galaxy.stardate < galaxy.final_stardate { + view::replay(); + let result = prompt(""); + if result.len() > 0 && result[0].to_uppercase() == "AYE" { + galaxy.enterprise = Galaxy::new_captain(&galaxy.quadrants); + continue; + } + } + break; + } else if galaxy.remaining_klingons() == 0 { + let efficiency = 1000.0 * f32::powi(initial_klingons as f32 / (galaxy.stardate - initial_stardate), 2); + view::congratulations(efficiency); + break; + } + } +} diff --git a/84_Super_Star_Trek/rust/src/model.rs b/84_Super_Star_Trek/rust/src/model.rs new file mode 100644 index 00000000..aa08d734 --- /dev/null +++ b/84_Super_Star_Trek/rust/src/model.rs @@ -0,0 +1,355 @@ +use std::{ops::{Mul, Add}, fmt::Display, collections::{HashMap, HashSet}}; + +use rand::Rng; + +use crate::view; + +pub struct Galaxy { + pub stardate: f32, + pub final_stardate: f32, + pub quadrants: Vec, + pub scanned: HashSet, + pub enterprise: Enterprise +} + +pub struct Quadrant { + pub stars: Vec, + pub star_base: Option, + pub klingons: Vec +} + +pub struct StarBase { + pub sector: Pos, + pub repair_delay: f32, +} + +pub struct Klingon { + pub sector: Pos, + pub energy: f32 +} + +impl Klingon { + pub fn fire_on(&mut self, enterprise: &mut Enterprise) { + let mut rng = rand::thread_rng(); + let attack_strength = rng.gen::(); + let dist_to_enterprise = self.sector.abs_diff(enterprise.sector) as f32; + let hit_strength = self.energy * (2.0 + attack_strength) / dist_to_enterprise; + + self.energy /= 3.0 + attack_strength; + + enterprise.take_hit(self.sector, hit_strength as u16); + } +} + +pub struct Enterprise { + pub destroyed: bool, + pub damaged: HashMap, + pub quadrant: Pos, + pub sector: Pos, + pub photon_torpedoes: u8, + pub total_energy: u16, + pub shields: u16, +} +impl Enterprise { + fn take_hit(&mut self, sector: Pos, hit_strength: u16) { + if self.destroyed { + return; + } + + view::enterprise_hit(&hit_strength, sector); + + if self.shields <= hit_strength { + view::enterprise_destroyed(); + self.destroyed = true; + return; + } + + self.shields -= hit_strength; + + view::shields_hit(self.shields); + + if hit_strength >= 20 { + self.take_damage(hit_strength) + } + } + + fn take_damage(&mut self, hit_strength: u16) { + let mut rng = rand::thread_rng(); + + let hit_past_shield = hit_strength as f32 / self.shields as f32; + if rng.gen::() > 0.6 || hit_past_shield < 0.02 { + return + } + + let system = systems::KEYS[rng.gen_range(0..systems::KEYS.len())].to_string(); + let damage = hit_past_shield + rng.gen::() * 0.5; + self.damage_system(&system, damage); + } + + pub fn damage_system(&mut self, system: &str, damage: f32) { + self.damaged.entry(system.to_string()).and_modify(|d| *d -= damage).or_insert(-damage); + } + + pub fn repair_system(&mut self, system: &str, amount: f32) -> bool { + let existing_damage = self.damaged[system]; + if existing_damage + amount >= -0.1 { + self.damaged.remove(system); + return true; + } + + self.damaged.entry(system.to_string()).and_modify(|d| *d += amount); + return false; + } + + pub fn is_stranded(&self) -> bool { + if self.total_energy < 10 || (self.shields + 10 > self.total_energy && self.damaged.contains_key(systems::SHIELD_CONTROL)) { + view::stranded(); + return true; + } + return false; + } +} + +pub mod systems { + + pub const SHORT_RANGE_SCAN: &str = "SRS"; + pub const WARP_ENGINES: &str = "NAV"; + pub const SHIELD_CONTROL: &str = "SHE"; + pub const DAMAGE_CONTROL: &str = "DAM"; + pub const LONG_RANGE_SCAN: &str = "LRS"; + pub const COMPUTER: &str = "COM"; + pub const PHASERS: &str = "PHA"; + pub const TORPEDOES: &str = "TOR"; + + pub const RESIGN: &str = "XXX"; + + pub const KEYS: [&str; 8] = [ + SHORT_RANGE_SCAN, WARP_ENGINES, SHIELD_CONTROL, DAMAGE_CONTROL, LONG_RANGE_SCAN, COMPUTER, PHASERS, TORPEDOES + ]; + + pub fn name_for(key: &str) -> String { + match key { + SHORT_RANGE_SCAN => "Short Range Scanners".into(), + WARP_ENGINES => "Warp Engines".into(), + SHIELD_CONTROL => "Shield Control".into(), + DAMAGE_CONTROL => "Damage Control".into(), + LONG_RANGE_SCAN => "Long Range Scanners".into(), + COMPUTER => "Library-Computer".into(), + PHASERS => "Phaser Control".into(), + TORPEDOES => "Photon Tubes".into(), + _ => "Unknown".into() + } + } +} + +#[derive(PartialEq, Clone, Copy, Debug, Hash, Eq)] +pub struct Pos(pub u8, pub u8); + +impl Pos { + pub fn as_index(&self) -> usize { + (self.0 * 8 + self.1).into() + } + + pub fn abs_diff(&self, other: Pos) -> u8 { + self.0.abs_diff(other.0) + self.1.abs_diff(other.1) + } + + pub fn dist(&self, other: Pos) -> u8 { + let dx = other.0 as f32 - self.0 as f32; + let dy = other.1 as f32 - self.1 as f32; + (f32::powi(dx, 2) + f32::powi(dy, 2)).sqrt() as u8 + } + + pub fn direction(&self, other: Pos) -> f32 { + // this is a replication of the original BASIC code + let dx = other.0 as f32 - self.0 as f32; + let dy = other.1 as f32 - self.1 as f32; + let dx_dominant = dx.abs() > dy.abs(); + + let frac = if dx_dominant { dy / dx } else { -dx / dy }; + let nearest_cardinal = + if dx_dominant { + if dx > 0. { 7. } else { 3. } + } else { + if dy > 0. { 1. } else { 5. } + }; + + let mut dir = nearest_cardinal + frac; + if dir < 1. { + dir += 8. + } + dir + } + + pub fn as_galactic_sector(&self, containing_quadrant: Pos) -> Self { + Pos(containing_quadrant.0 * 8 + self.0, containing_quadrant.1 * 8 + self.1) + } + + pub fn to_local_quadrant_sector(&self) -> (Self, Self) { + (Pos(self.0 / 8, self.1 / 8), Pos(self.0 % 8, self.1 % 8)) + } +} + +impl Mul for Pos { + type Output = Self; + + fn mul(self, rhs: u8) -> Self::Output { + Pos(self.0 * rhs, self.1 * rhs) + } +} + +impl Add for Pos { + type Output = Self; + + fn add(self, rhs: Pos) -> Self::Output { + Pos(self.0 + rhs.0, self.1 + rhs.1) + } +} + +impl Display for Pos { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} , {}", self.0 + 1, self.1 + 1) + } +} + +pub const COURSES : [(f32, f32); 9] = [ + (0., 1.), + (-1., 1.), + (-1., 0.), + (-1., -1.), + (0., -1.), + (1., -1.), + (1., 0.), + (1., 1.), + (0., 1.), // course 9 is equal to course 1 +]; + +#[derive(PartialEq)] +pub enum SectorStatus { + Empty, Star, StarBase, Klingon +} + +pub const MAX_PHOTON_TORPEDOES: u8 = 28; +pub const MAX_ENERGY: u16 = 3000; + +impl Galaxy { + pub fn remaining_klingons(&self) -> u8 { + let quadrants = &self.quadrants; + quadrants.into_iter().map(|q| { q.klingons.len() as u8 }).sum::() + } + + pub fn remaining_starbases(&self) -> u8 { + let quadrants = &self.quadrants; + quadrants.into_iter().filter(|q| q.star_base.is_some()).count() as u8 + } + + pub fn generate_new() -> Self { + let quadrants = Self::generate_quadrants(); + + let mut rng = rand::thread_rng(); + let stardate = rng.gen_range(20..=40) as f32 * 100.0; + + let enterprise = Self::new_captain(&quadrants); + + let mut scanned = HashSet::new(); + scanned.insert(enterprise.quadrant); + + Galaxy { + stardate, + final_stardate: stardate + rng.gen_range(25..=35) as f32, + quadrants, + scanned, + enterprise + } + } + + pub fn new_captain(quadrants: &Vec) -> Enterprise { + let mut rng = rand::thread_rng(); + let enterprise_quadrant = Pos(rng.gen_range(0..8), rng.gen_range(0..8)); + let enterprise_sector = quadrants[enterprise_quadrant.as_index()].find_empty_sector(); + Enterprise { + destroyed: false, + damaged: HashMap::new(), + quadrant: enterprise_quadrant, + sector: enterprise_sector, + photon_torpedoes: MAX_PHOTON_TORPEDOES, + total_energy: MAX_ENERGY, + shields: 0 } + } + + fn generate_quadrants() -> Vec { + let mut rng = rand::thread_rng(); + let mut result = Vec::new(); + for _ in 0..64 { + + let mut quadrant = Quadrant { stars: Vec::new(), star_base: None, klingons: Vec::new() }; + let star_count = rng.gen_range(0..=7); + for _ in 0..star_count { + quadrant.stars.push(quadrant.find_empty_sector()); + } + + if rng.gen::() > 0.96 { + quadrant.star_base = Some(StarBase { sector: quadrant.find_empty_sector(), repair_delay: rng.gen::() * 0.5 }); + } + + let klingon_count = + match rng.gen::() { + n if n > 0.98 => 3, + n if n > 0.95 => 2, + n if n > 0.8 => 1, + _ => 0 + }; + for _ in 0..klingon_count { + quadrant.klingons.push(Klingon { sector: quadrant.find_empty_sector(), energy: rng.gen_range(100..=300) as f32 }); + } + + result.push(quadrant); + } + result + } +} + +impl Quadrant { + pub fn sector_status(&self, sector: Pos) -> SectorStatus { + if self.stars.contains(§or) { + SectorStatus::Star + } else if self.is_starbase(sector) { + SectorStatus::StarBase + } else if self.has_klingon(sector) { + SectorStatus::Klingon + } else { + SectorStatus::Empty + } + } + + fn is_starbase(&self, sector: Pos) -> bool { + match &self.star_base { + None => false, + Some(p) => p.sector == sector + } + } + + fn has_klingon(&self, sector: Pos) -> bool { + let klingons = &self.klingons; + klingons.into_iter().find(|k| k.sector == sector).is_some() + } + + pub fn get_klingon(&mut self, sector: Pos) -> Option<&mut Klingon> { + let klingons = &mut self.klingons; + klingons.into_iter().find(|k| k.sector == sector) + } + + pub fn find_empty_sector(&self) -> Pos { + let mut rng = rand::thread_rng(); + loop { + let pos = Pos(rng.gen_range(0..8), rng.gen_range(0..8)); + if self.sector_status(pos) == SectorStatus::Empty { + return pos + } + } + } + + pub fn docked_at_starbase(&self, enterprise_sector: Pos) -> bool { + self.star_base.is_some() && self.star_base.as_ref().unwrap().sector.abs_diff(enterprise_sector) == 1 + } +} diff --git a/84_Super_Star_Trek/rust/src/view.rs b/84_Super_Star_Trek/rust/src/view.rs new file mode 100644 index 00000000..caf6b74d --- /dev/null +++ b/84_Super_Star_Trek/rust/src/view.rs @@ -0,0 +1,635 @@ +use crate::model::{Galaxy, Pos, SectorStatus, Enterprise, systems}; + +pub mod prompts { + pub const INSTRUCTIONS: &str = "Do you need instructions"; + pub const COURSE: &str = "Course (1-9)?"; + pub const TORPEDO_COURSE: &str = "Photon torpedo course (1-9)?"; + pub const SHIELDS: &str = "Number of units to shields"; + pub const REPAIR: &str = "Will you authorize the repair order"; + pub const COMPUTER: &str = "Computer active and waiting command?"; + pub const PHASERS: &str = "Number of units to fire"; + pub const WHEN_READY: &str = "Press Enter when ready to accept command"; + pub const COMMAND: &str = "Command?"; + pub const INITIAL_COORDS: &str = " Initial coordinates (X/Y)"; + pub const TARGET_COORDS: &str = " Final coordinates (X/Y)"; + + pub fn warp_factor(max_warp: f32) -> String { + format!("Warp Factor (0-{})?", max_warp) + } +} + +pub fn title() { + println!(" + + + + + + + + + + + + ************************************* + * * + * * + * * * SUPER STAR TREK * * * + * * + * * + ************************************* + + + + + + + + + + "); +} + +pub fn full_instructions() { + println!( +" INSTRUCTIONS FOR 'SUPER STAR TREK' + + 1. When you see \"Command ?\" printed, enter one of the legal + commands (NAV, SRS, LRS, PHA, TOR, SHE, DAM, COM, OR XXX). + 2. If you should type in an illegal command, you'll get a short + list of the legal commands printed out. + 3. Some commands require you to enter data (for example, the + 'NAV' command comes back with 'Course (1-9) ?'.) If you + type in illegal data (like negative numbers), then command + will be aborted. + + The galaxy is divided into an 8 X 8 quadrant grid, + and each quadrant is further divided into an 8 X 8 sector grid. + + You will be assigned a starting point somewhere in the + galaxy to begin a tour of duty as commander of the starship + Enterprise; your mission: to seek and destroy the fleet of + Klingon warships which are menacing the United Federation of + Planets. + + You have the following commands available to you as captain + of the starship Enterprise: + + NAV command = Warp Engine Control + Course is in a circular numerical 4 3 2 + vector arrangement as shown . . . + integer and real values may be ... + used. (Thus course 1.5 is half- 5 ---*--- 1 + way between 1 and 2. ... + . . . + Values may approach 9.0, which 6 7 8 + itself is equivalent to 1.0 + COURSE + One warp factor is the size of + one quadrant. Therefore, to get + from quadrant 6,5 to 5,5, you WOULD + use course 3, warp factor 1. + + SRS command = Short Range Sensor Scan + Shows you a scan of your present quadrant. + + Symbology on your sensor screen is as follows: + <*> = Your starship's position + +K+ = Klingon battle cruiser + >!< = Federation starbase (refuel/repair/re-arm here!) + * = Star + + A condensed 'status report' will also be presented. + + LRS command = Long Range Sensor Scan + Shows conditions in space for one quadrant on each side + of the Enterprise (which is in the middle of the scan). + The scan is coded in the form ###, where the units digit + is the number of stars, the tens digit is the number of + starbases, and the hundreds digit is the number of + Klingons. + + Example - 207 = 2 Klingons, No starbases, & 7 stars. + + PHA command = Phaser Control + Allows you to destroy the Klingon battle cruisers by + zapping them with suitably large units of energy to + deplete their shield power. (Remember, Klingons have + phasers, too!) + + TOR command = Photon Torpedo Control + Torpedo course is the same as used in warp engine control. + If you hit the Klingon vessel, he is destroyed and + cannot fire back at you. If you miss, you are subject to + his phaser fire. In either case, you are also subject to + the phaser fire of all other Klingons in the quadrant. + + The library-computer (COM command) has an option to + compute torpedo trajectory for you (Option 2). + + SHE command = Shield Control + Defines the number of energy units to be assigned to the + shields. Energy is taken from total ship's energy. Note + that the status display total energy includes shield energy. + + DAM command = Damage Control Report + Gives the state of repair of all devices. Where a negative + 'state of repair' shows that the device is temporarily + damaged. + + COM command = Library-Computer + The library-computer contains six options: + Option 0 = Cumulative Galactic Record + This option shows computer memory of the results of all + previous short and long range sensor scans. + Option 1 = Status Report + This option shows the number of Klingons, Stardates, + and starbases remaining in the game. + Option 2 = Photon Torpedo Data + Which gives directions and distance from the Enterprise + to all Klingons in your quadrant. + Option 3 = Starbase Nav Data + This option gives direction and distance to any + starbase within your quadrant. + Option 4 = Direction/Distance Calculator + This option allows you to enter coordinates for + direction/distance calculations. + Option 5 = Galactic Region Name Map + This option prints the names of the sixteen major + galactic regions referred to in the game. + +") +} + +pub fn enterprise() { + println!(" + + + + + + + + + + + ,------*------, + ,------------- '--- ------' + '-------- --' / / + ,---' '-------/ /--, + '----------------' + + THE USS ENTERPRISE --- NCC-1701 + + + + + + +") +} + +pub fn intro(model: &Galaxy) { + let star_bases = model.remaining_starbases(); + let mut star_base_message: String = "There is 1 starbase".into(); + if star_bases > 1 { + star_base_message = format!("There are {} starbases", star_bases); + } + println!( +"Your orders are as follows: + Destroy the {} Klingon warships which have invaded + the galaxy before they can attack federation headquarters + on stardate {:.1}. This gives you {} days. {} in the galaxy for resupplying your ship.\n", + model.remaining_klingons(), model.final_stardate, model.final_stardate - model.stardate, star_base_message) +} + +const REGION_NAMES: [&str; 16] = [ + "Antares", + "Sirius", + "Rigel", + "Deneb", + "Procyon", + "Capella", + "Vega", + "Betelgeuse", + "Canopus", + "Aldebaran", + "Altair", + "Regulus", + "Sagittarius", + "Arcturus", + "Pollux", + "Spica" +]; + +const SUB_REGION_NAMES: [&str; 4] = ["I", "II", "III", "IV"]; + +fn quadrant_name(quadrant: Pos) -> String { + format!("{} {}", + REGION_NAMES[((quadrant.0 << 1) + (quadrant.1 >> 2)) as usize], + SUB_REGION_NAMES[(quadrant.1 % 4) as usize]) +} + +pub fn starting_quadrant(quadrant: Pos) { + println!( +"\nYour mission begins with your starship located +in the galactic quadrant, '{}'.\n", quadrant_name(quadrant)) +} + +pub fn enter_quadrant(quadrant: Pos) { + println!("\nNow entering {} quadrant . . .\n", quadrant_name(quadrant)) +} + +pub fn short_range_scan(model: &Galaxy) { + let quadrant = &model.quadrants[model.enterprise.quadrant.as_index()]; + let mut condition = "GREEN"; + if quadrant.docked_at_starbase(model.enterprise.sector) { + println!("Shields dropped for docking purposes"); + condition = "DOCKED"; + } else if quadrant.klingons.len() > 0 { + condition = "*RED*"; + } else if model.enterprise.damaged.len() > 0 { + condition = "YELLOW"; + } + + let data : [String; 8] = [ + format!("Stardate {:.1}", model.stardate), + format!("Condition {}", condition), + format!("Quadrant {}", model.enterprise.quadrant), + format!("Sector {}", model.enterprise.sector), + format!("Photon torpedoes {}", model.enterprise.photon_torpedoes), + format!("Total energy {}", model.enterprise.total_energy), + format!("Shields {}", model.enterprise.shields), + format!("Klingons remaining {}", model.remaining_klingons()), + ]; + + println!("{:-^33}", ""); + for x in 0..=7 { + for y in 0..=7 { + let pos = Pos(x, y); + if &pos == &model.enterprise.sector { + print!("<*> ") + } else { + match quadrant.sector_status(pos) { + SectorStatus::Star => print!(" * "), + SectorStatus::StarBase => print!(">!< "), + SectorStatus::Klingon => print!("+K+ "), + _ => print!(" "), + } + } + } + println!("{:>9}{}", "", data[x as usize]) + } + println!("{:-^33}", ""); +} + +pub fn print_command_help() { + println!( +"Enter one of the following: + NAV (To set course) + SRS (For short range sensor scan) + LRS (For long range sensor scan) + PHA (To fire phasers) + TOR (To fire photon torpedoes) + SHE (To raise or lower shields) + DAM (For damage control reports) + COM (To call on library-computer) + XXX (To resign your command) + ") +} + +pub fn end_game_failure(galaxy: &Galaxy) { + println!( +"Is is stardate {:.1}. +There were {} Klingon battle cruisers left at +the end of your mission. +", galaxy.stardate, galaxy.remaining_klingons()); +} + +pub fn enterprise_destroyed() { + println!("The Enterprise has been destroyed. The Federation will be conquered."); +} + +pub fn bad_course_data() { + println!(" Lt. Sulu reports, 'Incorrect course data, sir!'") +} + +pub fn bad_nav(current_sector: Pos) { + println!("Warp engines shut down at sector {current_sector} dues to bad navigation") +} + +pub fn bad_torpedo_course() { + println!(" Ensign Chekov reports, 'Incorrect course data, sir!'") +} + +pub fn enterprise_hit(hit_strength: &u16, from_sector: Pos) { + println!("{hit_strength} unit hit on Enterprise from sector {from_sector}"); +} + +pub fn hit_edge(quadrant: Pos, sector: Pos) { + println!( +"Lt. Uhura report message from Starfleet Command: + 'Permission to attempt crossing of galactic perimeter + is hereby *Denied*. Shut down your engines.' + Chief Engineer Scott reports, 'Warp engines shut down + at sector {} of quadrant {}.'", sector, quadrant); +} + +pub fn condition_red() { + println!("COMBAT AREA CONDITION RED") +} + +pub fn danger_shields() { + println!(" SHIELDS DANGEROUSLY LOW ") +} + +pub fn insuffient_warp_energy(warp_speed: f32) { + println!( +"Engineering reports, 'Insufficient energy available + for maneuvering at warp {warp_speed} !'") +} + +pub fn divert_energy_from_shields() { + println!("Shield Control supplies energy to complete the maneuver.") +} + +pub fn energy_available(total_energy: u16) { + println!("Energy available = {{{total_energy}}}") +} + +pub fn shields_unchanged() { + println!("") +} + +pub fn ridiculous() { + println!("Shield Control reports, 'This is not the Federation Treasury.'") +} + +pub fn shields_set(value: u16) { + println!( +"Deflector control room report: + 'Shields now at {value} units per your command.'") +} + +pub fn shields_hit(shields: u16) { + println!(" ") +} + +pub fn inoperable(arg: &str) { + println!("{} inoperable", arg) +} + +pub fn scanners_out() { + println!("*** Short Range Sensors are out ***") +} + +pub fn damaged_engines(max_warp: f32, warp_factor: f32) { + println!( +"Warp engines are damaged. Maximum speed = warp {max_warp} + Chief Engineer Scott reports, 'The engines won't take warp {warp_factor} !'") +} + +pub fn damage_control_report() { + println!("Damage Control report:") +} + +pub fn random_repair_report_for(name: &str, damaged: bool) { + let mut message = "state of repair improved"; + if damaged { + message = "damaged"; + } + println!("Damage Control report: {name} {message}") +} + +pub fn system_repair_completed(name: String) { + println!(" {name} repair completed.") +} + +pub fn damage_control(enterprise: &Enterprise) { + println!("Device State of Repair"); + for key in systems::KEYS { + let damage = enterprise.damaged.get(key).unwrap_or(&0.0); + println!("{:<25}{}", systems::name_for(key), damage) + } + println!(); +} + +pub fn long_range_scan(galaxy: &Galaxy) -> Vec { + + let cx = galaxy.enterprise.quadrant.0 as i8; + let cy = galaxy.enterprise.quadrant.1 as i8; + + let mut seen = Vec::new(); + + println!("Long range scan for quadrant {}", galaxy.enterprise.quadrant); + println!("{:-^19}", ""); + for x in cx - 1..=cx + 1 { + for y in cy - 1..=cy + 1 { + let mut klingons = "*".into(); + let mut star_bases = "*".into(); + let mut stars = "*".into(); + + if y >= 0 && y < 8 && x >= 0 && x < 8 { + let pos = Pos(x as u8, y as u8); + seen.push(pos); + + let quadrant = &galaxy.quadrants[pos.as_index()]; + klingons = format!("{}", quadrant.klingons.len()); + star_bases = quadrant.star_base.as_ref().map_or("0", |_| "1"); + stars = format!("{}", quadrant.stars.len()); + } + + print!(": {}{}{} ", klingons, star_bases, stars) + } + println!(":"); + println!("{:-^19}", ""); + } + + seen +} + +pub fn stranded() { + println!( +"** FATAL ERROR ** You've just stranded your ship in space +You have insufficient maneuvering energy, and shield control +is presently incapable of cross-circuiting to engine room!!") +} + +pub fn computer_options() { + println!( +" 0 = Cumulative galactic record + 1 = Status report + 2 = Photon torpedo data + 3 = Starbase nav data + 4 = Direction/distance calculator + 5 = Galaxy 'region name' map") +} + +pub fn galaxy_region_map() { + println!( +" The Galaxy + 1 2 3 4 5 6 7 8 + ----- ----- ----- ----- ----- ----- ----- -----"); + for i in (0..REGION_NAMES.len()-1).step_by(2) { + println!( +"{} {:^23} {:^23} + ----- ----- ----- ----- ----- ----- ----- -----", (i/2)+1, REGION_NAMES[i], REGION_NAMES[i+1]); + } +} + +pub fn galaxy_scanned_map(galaxy: &Galaxy) { + println!( +"Computer record of galaxy for quadrant {} + 1 2 3 4 5 6 7 8 + ----- ----- ----- ----- ----- ----- ----- -----", galaxy.enterprise.quadrant); + for x in 0..8 { + print!("{} ", x+1); + for y in 0..8 { + let pos = Pos(x, y); + if galaxy.scanned.contains(&pos) { + let quadrant = &galaxy.quadrants[pos.as_index()]; + print!(" {}{}{} ", quadrant.klingons.len(), quadrant.star_base.as_ref().map_or("0", |_| "1"), quadrant.stars.len()) + } else { + print!(" *** "); + } + } + println!( +"\n ----- ----- ----- ----- ----- ----- ----- -----") + } +} + +pub fn no_local_enemies() { + println!( +"Science Officer Spock reports, 'Sensors show no enemy ships + in this quadrant'") +} + +pub fn computer_accuracy_issue() { + println!("Computer failure hampers accuracy") +} + +pub fn phasers_locked(available_energy: u16) { + println!("Phasers locked on target; Energy available = {available_energy} units") +} + +pub fn starbase_shields() { + println!("Starbase shields protect the Enterprise") +} + +pub fn repair_estimate(repair_time: f32) { + println!( +"Technicians standing by to effect repairs to your ship; +Estimated time to repair: {repair_time:.1} stardates.") +} + +pub fn no_damage(sector: Pos) { + println!("Sensors show no damage to enemy at {sector}") +} + +pub fn hit_on_klingon(hit_strength: f32, sector: Pos) { + println!("{hit_strength} unit hit on Klingon at sector {sector}") +} + +pub fn klingon_remaining_energy(energy: f32) { + println!(" (sensors show {energy} units remaining)") +} + +pub fn klingon_destroyed() { + println!("*** Klingon destroyed ***") +} + +pub fn congratulations(efficiency: f32) { + println!(" +Congratulations, Captain! The last Klingon battle cruiser +menacing the Federation has been destroyed. + +Your efficiency rating is {efficiency}. + ") +} + +pub fn replay() { + println!(" +The Federation is in need of a new starship commander +for a similar mission -- if there is a volunteer +let him step forward and enter 'Aye'") +} + +pub fn no_torpedoes_remaining() { + println!("All photon torpedoes expended") +} + +pub fn torpedo_track() { + println!("Torpedo track:") +} + +pub fn torpedo_path(sector: Pos) { + println!("{:<16}{}", "", sector) +} + +pub fn torpedo_missed() { + println!("Torpedo missed!") +} + +pub fn star_absorbed_torpedo(sector: Pos) { + println!("Star at {sector} absorbed torpedo energy.") +} + +pub fn destroyed_starbase(not_the_last_starbase: bool) { + println!("*** Starbase destroyed ***"); + if not_the_last_starbase { + println!(" +Starfleet Command reviewing your record to consider +court martial!") + } else { + println!(" +That does it, Captain!! You are hereby relieved of command +and sentenced to 99 stardates at hard labor on Cygnus 12!!") + } +} + +pub fn no_local_starbase() { + println!("Mr. Spock reports, 'Sensors show no starbases in this quadrant.'") +} + +pub fn starbase_report() { + println!("From Enterprise to Starbase:'") +} + +pub fn direction_distance(dir: f32, dist: u8) { + println!( +"Direction = {dir} +Distance = {dist}" + ) +} + +pub fn status_report(galaxy: &Galaxy) { + let klingon_count = galaxy.remaining_klingons(); + let star_bases = galaxy.remaining_starbases(); + let time_remaining = galaxy.final_stardate - galaxy.stardate; + let mut plural_starbase = ""; + if star_bases > 1 { + plural_starbase = "s"; + } + + println!(" Status report: +Klingons left: {klingon_count} +Mission must be completed in {time_remaining:.1} stardates. +The Federation is maintaining {star_bases} starbase{plural_starbase} in the galaxy. +") +} + +pub fn direction_dist_intro(enterprise: &Enterprise) { + let quadrant = enterprise.quadrant; + let sector = enterprise.sector; + println!("Direction/distance calculator: +You are at quadrant {quadrant} sector {sector} +Please enter") +} + +pub fn klingon_report(more_than_one: bool) { + let mut plural = ""; + if more_than_one { + plural = "s"; + } + println!("From Enterprise to Klingon battle cruiser{}", plural) +} \ No newline at end of file diff --git a/86_Target/perl/README.md b/86_Target/perl/README.md index e69c8b81..eca6b9fa 100644 --- a/86_Target/perl/README.md +++ b/86_Target/perl/README.md @@ -1,3 +1,9 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) Conversion to [Perl](https://www.perl.org/) + +Modified so that if the user enters "quit" or "stop" for the input, the program will exit. +This way the user doesn't have to enter Contorl-C to quit. + +Target values can be space and/or comma separated, so "1 2 3" is valid, as is "1,2,3" or even "1, 2, 3". +I believe the original Basic program wanted "1,2,3" or else each on a separate line. diff --git a/86_Target/perl/target.pl b/86_Target/perl/target.pl new file mode 100644 index 00000000..cb552a39 --- /dev/null +++ b/86_Target/perl/target.pl @@ -0,0 +1,107 @@ +#!/usr/bin/perl + +# Target program in Perl +# Modified so that if the user enters "quit" or "stop" for the input, the program will exit. +# Values can be space and/or comma separated. +# Translated by Kevin Brannen (kbrannen) + +use strict; +use warnings; + +# globals +my $R = 1; +my $R1 = 57.296; +my $Pi = 3.14159; + +print "\n"; +print " " x 33, "TARGET\n"; +print " " x 15, "CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n"; + +print "YOU ARE THE WEAPONS OFFICER ON THE STARSHIP ENTERPRISE\n"; +print "AND THIS IS A TEST TO SEE HOW ACCURATE A SHOT YOU\n"; +print "ARE IN A THREE-DIMENSIONAL RANGE. YOU WILL BE TOLD\n"; +print "THE RADIAN OFFSET FOR THE X AND Z AXES, THE LOCATION\n"; +print "OF THE TARGET IN THREE DIMENSIONAL RECTANGULAR COORDINATES,\n"; +print "THE APPROXIMATE NUMBER OF DEGREES FROM THE X AND Z\n"; +print "AXES, AND THE APPROXIMATE DISTANCE TO THE TARGET.\n"; +print "YOU WILL THEN PROCEEED TO SHOOT AT THE TARGET UNTIL IT IS\n"; +print "DESTROYED!\n\n"; +print "GOOD LUCK!!\n\n"; + +while (1) +{ + my $A = rand(1) * 2 * $Pi; + my $B = rand(1) * 2 * $Pi; + my $P1 = 100000 * rand(1) + rand(1); + my $X = sin($B) * cos($A) * $P1; + my $Y = sin($B) * sin($A) * $P1; + my $Z = cos($B) * $P1; + print "RADIANS FROM X AXIS = $A FROM Z AXIS = $B\n"; + print "TARGET SIGHTED: APPROXIMATE COORDINATES: X=$X Y=$Y Z=$Z\n"; + + while (1) + { + my $P3; + $R++; + + if ($R == 1) { $P3 = int($P1 * .05) * 20; } + elsif ($R == 2) { $P3 = int($P1 * .1) * 10; } + elsif ($R == 3) { $P3 = int($P1 * .5) * 2; } + elsif ($R == 4) { $P3 = int($P1); } + else { $P3 = $P1; } + + print " ESTIMATED DISTANCE: $P3\n\n"; + print "INPUT ANGLE DEVIATION FROM X, DEVIATION FROM Z, DISTANCE: "; + chomp(my $ans = lc(<>)); + exit(0) if ($ans eq "quit" || $ans eq "stop"); + + my ($A1, $B1, $P2) = split(/[,\s]+/, $ans); + print "\n"; + + if ($P2 >= 20) + { + $A1 /= $R1; + $B1 /= $R1; + print "RADIANS FROM X AXIS = $A1 FROM Z AXIS = $B1\n"; + my $X1 = $P2 * sin($B1) * cos($A1); + my $Y1 = $P2 * sin($B1) * sin($A1); + my $Z1 = $P2 * cos($B1); + my $D = (($X1 - $X) ** 2 + ($Y1 - $Y) ** 2 + ($Z1 - $Z) ** 2) ** (0.5); + + if ($D <= 20) + { + print "\n * * * HIT * * * TARGET IS NON-FUNCTIONAL\n"; + print "\nDISTANCE OF EXPLOSION FROM TARGET WAS $D KILOMETERS.\n"; + print "\nMISSION ACCOMPLISHED IN $R SHOTS.\n"; + last; + } + else + { + my $X2 = $X1 - $X; + my $Y2 = $Y1 - $Y; + my $Z2 = $Z1 - $Z; + + if ($X2 < 0) { print "SHOT BEHIND TARGET ", -$X2, " KILOMETERS.\n"; } + else { print "SHOT IN FRONT OF TARGET $X2 KILOMETERS.\n"; } + + if ($Y2 < 0) { print "SHOT TO RIGHT OF TARGET ", -$Y2, " KILOMETERS.\n"; } + else { print "SHOT TO LEFT OF TARGET $Y2 KILOMETERS.\n"; } + + if ($Z2 < 0) { print "SHOT BELOW TARGET ", -$Z2, " KILOMETERS.\n"; } + else { print "SHOT ABOVE TARGET $Z2 KILOMETERS.\n"; } + + print "APPROX POSITION OF EXPLOSION: X=$X1 Y=$Y1 Z=$Z1\n"; + print " DISTANCE FROM TARGET = $D\n\n\n"; + next; + } + } + else + { + print "YOU BLEW YOURSELF UP!!\n"; + last; + } + } + + $R = 0; + print "\n\n\n\n\nNEXT TARGET...\n\n"; +}