mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 23:26:40 -08:00
Merge branch 'main' of https://github.com/spasticus74/basic-computer-games
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::ffi::{OsString, OsStr};
|
||||
use std::ffi::OsStr;
|
||||
use std::{fs, io};
|
||||
use std::fs::metadata;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -8,8 +8,6 @@ use std::path::{Path, PathBuf};
|
||||
* @author Anthony Rubick
|
||||
*/
|
||||
|
||||
|
||||
|
||||
//DATA
|
||||
const ROOT_DIR: &str = "../../";
|
||||
const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language name, second element is the file extension
|
||||
@@ -25,13 +23,19 @@ const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language
|
||||
("vbnet", "vb")
|
||||
];
|
||||
const OUTPUT_PATH: &str = "../../todo.md";
|
||||
//const INGORE: [&str;5] = ["../../.git","../../.vscode","../../00_Utilities","../../buildJvm","../../node_modules"]; //folders to ignore
|
||||
|
||||
fn main() {
|
||||
//DATA
|
||||
let mut root_folders:Vec<PathBuf>;
|
||||
let mut output_string: String = String::new();
|
||||
let format_game_first: bool;
|
||||
let ingore: [PathBuf;5] = [
|
||||
PathBuf::from(r"../../.git"),
|
||||
PathBuf::from(r"../../.github"),
|
||||
PathBuf::from(r"../../00_Alternate_Languages"),
|
||||
PathBuf::from(r"../../00_Utilities"),
|
||||
PathBuf::from(r"../../00_Common"),
|
||||
]; //folders to ignore
|
||||
|
||||
//print welcome message
|
||||
println!("
|
||||
@@ -66,15 +70,10 @@ fn main() {
|
||||
|
||||
//for all folders, search for the languages and extensions
|
||||
root_folders = root_folders.into_iter().filter(|path| {
|
||||
match fs::read_dir(path) {
|
||||
Err(why) => {println!("! {:?}", why.kind()); false},
|
||||
Ok(paths) => {
|
||||
paths.into_iter().filter(|f| metadata(f.as_ref().unwrap().path()).unwrap().is_dir()) //filter to only folders
|
||||
.filter_map( |path| path.ok() ) //extract only the DirEntries
|
||||
.any(|f| LANGUAGES.iter().any(|tup| OsString::from(tup.1).eq_ignore_ascii_case(f.file_name()))) //filter out ones that don't contain folders with the language names
|
||||
}
|
||||
}
|
||||
//not one of the ignored folders
|
||||
!ingore.contains(path)
|
||||
}).collect();
|
||||
root_folders.sort();
|
||||
|
||||
//create todo list
|
||||
if format_game_first {
|
||||
|
||||
8
21_Calendar/rust/Cargo.toml
Normal file
8
21_Calendar/rust/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
3
21_Calendar/rust/README.md
Normal file
3
21_Calendar/rust/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html)
|
||||
|
||||
Conversion to [Rust](https://www.rust-lang.org/) by [Uğur Küpeli](https://github.com/ugurkupeli)
|
||||
152
21_Calendar/rust/src/main.rs
Normal file
152
21_Calendar/rust/src/main.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use std::io::stdin;
|
||||
|
||||
const WIDTH: usize = 64;
|
||||
const DAYS_WIDTH: usize = WIDTH / 8;
|
||||
const MONTH_WIDTH: usize = WIDTH - (DAYS_WIDTH * 2);
|
||||
const DAY_NUMS_WIDTH: usize = WIDTH / 7;
|
||||
|
||||
const DAYS: [&str; 7] = [
|
||||
"SUNDAY",
|
||||
"MONDAY",
|
||||
"TUESDAY",
|
||||
"WEDNESDAY",
|
||||
"THURSDAY",
|
||||
"FRIDAY",
|
||||
"SATURDAY",
|
||||
];
|
||||
|
||||
fn main() {
|
||||
println!("\n\t\t CALENDAR");
|
||||
println!("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n");
|
||||
|
||||
let (starting_day, leap_year) = prompt();
|
||||
let (months, total_days) = get_months_and_days(leap_year);
|
||||
|
||||
let mut days_passed = 0;
|
||||
let mut current_day_index = DAYS.iter().position(|d| *d == starting_day).unwrap();
|
||||
|
||||
for (month, days) in months {
|
||||
print_header(month, days_passed, total_days - days_passed);
|
||||
print_days(&mut current_day_index, days);
|
||||
days_passed += days as u16;
|
||||
println!("\n");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt() -> (String, bool) {
|
||||
let mut day = String::new();
|
||||
|
||||
loop {
|
||||
println!("\nFirst day of the year?");
|
||||
if let Ok(_) = stdin().read_line(&mut day) {
|
||||
day = day.trim().to_uppercase();
|
||||
if DAYS.contains(&day.as_str()) {
|
||||
break;
|
||||
} else {
|
||||
day.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut leap = false;
|
||||
|
||||
loop {
|
||||
println!("Is this a leap year?");
|
||||
let mut input = String::new();
|
||||
if let Ok(_) = stdin().read_line(&mut input) {
|
||||
match input.to_uppercase().trim() {
|
||||
"Y" | "YES" => {
|
||||
leap = true;
|
||||
break;
|
||||
}
|
||||
"N" | "NO" => break,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
(day, leap)
|
||||
}
|
||||
|
||||
fn get_months_and_days(leap_year: bool) -> (Vec<(String, u8)>, u16) {
|
||||
let months = [
|
||||
"JANUARY",
|
||||
"FEBUARY",
|
||||
"MARCH",
|
||||
"APRIL",
|
||||
"MAY",
|
||||
"JUNE",
|
||||
"JULY",
|
||||
"AUGUST",
|
||||
"SEPTEMBER",
|
||||
"OCTOBER",
|
||||
"NOVEMBER",
|
||||
"DECEMBER",
|
||||
];
|
||||
|
||||
let mut months_with_days = Vec::new();
|
||||
let mut total_days: u16 = 0;
|
||||
|
||||
for (i, month) in months.iter().enumerate() {
|
||||
let days = if i == 1 {
|
||||
if leap_year {
|
||||
29u8
|
||||
} else {
|
||||
28
|
||||
}
|
||||
} else if if i < 7 { (i % 2) == 0 } else { (i % 2) != 0 } {
|
||||
31
|
||||
} else {
|
||||
30
|
||||
};
|
||||
|
||||
total_days += days as u16;
|
||||
months_with_days.push((month.to_string(), days));
|
||||
}
|
||||
|
||||
(months_with_days, total_days)
|
||||
}
|
||||
|
||||
fn print_between(s: String, w: usize, star: bool) {
|
||||
let s = format!(" {s} ");
|
||||
if star {
|
||||
print!("{:*^w$}", s);
|
||||
return;
|
||||
}
|
||||
print!("{:^w$}", s);
|
||||
}
|
||||
|
||||
fn print_header(month: String, days_passed: u16, days_left: u16) {
|
||||
print_between(days_passed.to_string(), DAYS_WIDTH, true);
|
||||
print_between(month.to_string(), MONTH_WIDTH, true);
|
||||
print_between(days_left.to_string(), DAYS_WIDTH, true);
|
||||
println!();
|
||||
|
||||
for d in DAYS {
|
||||
let d = d.chars().nth(0).unwrap();
|
||||
print_between(d.to_string(), DAY_NUMS_WIDTH, false);
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("{:*>WIDTH$}", "");
|
||||
}
|
||||
|
||||
fn print_days(current_day_index: &mut usize, days: u8) {
|
||||
let mut current_date = 1u8;
|
||||
|
||||
print!("{:>w$}", " ", w = DAY_NUMS_WIDTH * *current_day_index);
|
||||
|
||||
for _ in 1..=days {
|
||||
print_between(current_date.to_string(), DAY_NUMS_WIDTH, false);
|
||||
|
||||
if ((*current_day_index + 1) % 7) == 0 {
|
||||
*current_day_index = 0;
|
||||
println!();
|
||||
} else {
|
||||
*current_day_index += 1;
|
||||
}
|
||||
|
||||
current_date += 1;
|
||||
}
|
||||
}
|
||||
@@ -17,5 +17,7 @@ http://www.vintage-basic.net/games.html
|
||||
|
||||
(please note any difficulties or challenges in porting here)
|
||||
|
||||
There is a type in the original Basic, "...DECIDE **WHO** MUCH WATER..." should be "DECIDE **HOW** MUCH WATER"
|
||||
|
||||
#### External Links
|
||||
- C: https://github.com/ericfischer/basic-computer-games/blob/main/24%20Chemist/c/chemist.c
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html)
|
||||
|
||||
Conversion to [Lua](https://www.lua.org/)
|
||||
Conversion to [Lua](https://www.lua.org/) by Alex Conconi
|
||||
|
||||
---
|
||||
|
||||
### Lua porting notes
|
||||
|
||||
- The `craps_main` function contains the main game loop, which iteratively
|
||||
plays craps rounds by calling `play_round` and tracks winnings and losings.
|
||||
- Replaced the original routine that tries to scramble the random number
|
||||
generator with a proper seed initializer in Lua: `math.randomseed(os.time())`
|
||||
(as advised in the general porting notes).
|
||||
- Added basic input validation to accept only positive integers for the
|
||||
wager and the answer to the "If you want to play again print 5" question.
|
||||
- "If you want to play again print 5 if not print 2" reads a bit odd but
|
||||
we decided to leave it as is and stay true to the BASIC original version.
|
||||
149
29_Craps/lua/craps.lua
Normal file
149
29_Craps/lua/craps.lua
Normal file
@@ -0,0 +1,149 @@
|
||||
--[[
|
||||
Craps
|
||||
|
||||
From: BASIC Computer Games (1978)
|
||||
Edited by David H. Ahl
|
||||
|
||||
This game simulates the games of craps played according to standard
|
||||
Nevada craps table rules. That is:
|
||||
1. A 7 or 11 on the first roll wins
|
||||
2. A 2, 3, or 12 on the first roll loses
|
||||
3. Any other number rolled becomes your "point." You continue to roll;
|
||||
if you get your point you win. If you roll a 7, you lose and the dice
|
||||
change hands when this happens.
|
||||
|
||||
This version of craps was modified by Steve North of Creative Computing.
|
||||
It is based on an original which appeared one day one a computer at DEC.
|
||||
|
||||
|
||||
Lua port by Alex Conconi, 2022
|
||||
--]]
|
||||
|
||||
|
||||
--- Throw two dice and return their sum.
|
||||
local function throw_dice()
|
||||
return math.random(1, 6) + math.random(1, 6)
|
||||
end
|
||||
|
||||
|
||||
--- Print prompt and read a number > 0 from stdin.
|
||||
local function input_number(prompt)
|
||||
while true do
|
||||
io.write(prompt)
|
||||
local number = tonumber(io.stdin:read("*l"))
|
||||
if number and number > 0 then
|
||||
return number
|
||||
else
|
||||
print("Please enter a number greater than zero.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Print custom balance message depending on balance value
|
||||
local function print_balance(balance, under_msg, ahead_msg, even_msg)
|
||||
if balance < 0 then
|
||||
print(under_msg)
|
||||
elseif balance > 0 then
|
||||
print(ahead_msg)
|
||||
else
|
||||
print(even_msg)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Play a round and return winnings or losings.
|
||||
local function play_round()
|
||||
-- Input the wager
|
||||
local wager = input_number("Input the amount of your wager: ")
|
||||
|
||||
-- Roll the die for the first time.
|
||||
print("I will now throw the dice")
|
||||
local first_roll = throw_dice()
|
||||
|
||||
-- A 7 or 11 on the first roll wins.
|
||||
if first_roll == 7 or first_roll == 11 then
|
||||
print(string.format("%d - natural.... a winner!!!!", first_roll))
|
||||
print(string.format("%d pays even money, you win %d dollars", first_roll, wager))
|
||||
return wager
|
||||
end
|
||||
|
||||
-- A 2, 3, or 12 on the first roll loses.
|
||||
if first_roll == 2 or first_roll == 3 or first_roll == 12 then
|
||||
if first_roll == 2 then
|
||||
-- Special 'you lose' message for 'snake eyes'
|
||||
print(string.format("%d - snake eyes.... you lose.", first_roll))
|
||||
else
|
||||
-- Default 'you lose' message
|
||||
print(string.format("%d - craps.... you lose.", first_roll))
|
||||
end
|
||||
print(string.format("You lose %d dollars", wager))
|
||||
return -wager
|
||||
end
|
||||
|
||||
-- Any other number rolled becomes the "point".
|
||||
-- Continue to roll until rolling a 7 or point.
|
||||
print(string.format("%d is the point. I will roll again", first_roll))
|
||||
while true do
|
||||
local second_roll = throw_dice()
|
||||
if second_roll == first_roll then
|
||||
-- Player gets point and wins
|
||||
print(string.format("%d - a winner.........congrats!!!!!!!!", first_roll))
|
||||
print(string.format("%d at 2 to 1 odds pays you...let me see... %d dollars", first_roll, 2 * wager))
|
||||
return 2 * wager
|
||||
end
|
||||
if second_roll == 7 then
|
||||
-- Player rolls a 7 and loses
|
||||
print(string.format("%d - craps. You lose.", second_roll))
|
||||
print(string.format("You lose $ %d", wager))
|
||||
return -wager
|
||||
end
|
||||
-- Continue to roll
|
||||
print(string.format("%d - no point. I will roll again", second_roll))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Main game function.
|
||||
local function craps_main()
|
||||
-- Print the introduction to the game
|
||||
print(string.rep(" ", 32) .. "Craps")
|
||||
print(string.rep(" ", 14) .. "Creative Computing Morristown, New Jersey\n\n")
|
||||
print("2,3,12 are losers; 4,5,6,8,9,10 are points; 7,11 are natural winners.")
|
||||
|
||||
-- Initialize random number generator seed
|
||||
math.randomseed(os.time())
|
||||
|
||||
-- Initialize balance to track winnings and losings
|
||||
local balance = 0
|
||||
|
||||
-- Main game loop
|
||||
local keep_playing = true
|
||||
while keep_playing do
|
||||
-- Play a round
|
||||
balance = balance + play_round()
|
||||
|
||||
-- If player's answer is 5, then stop playing
|
||||
keep_playing = (input_number("If you want to play again print 5 if not print 2: ") == 5)
|
||||
|
||||
-- Print an update on winnings or losings
|
||||
print_balance(
|
||||
balance,
|
||||
string.format("You are now under $%d", -balance),
|
||||
string.format("You are now ahead $%d", balance),
|
||||
"You are now even at 0"
|
||||
)
|
||||
end
|
||||
|
||||
-- Game over, print the goodbye message
|
||||
print_balance(
|
||||
balance,
|
||||
"Too bad, you are in the hole. Come again.",
|
||||
"Congratulations---you came out a winner. Come again.",
|
||||
"Congratulations---you came out even, not bad for an amateur"
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
--- Run the game.
|
||||
craps_main()
|
||||
8
55_Life/rust/Cargo.toml
Normal file
8
55_Life/rust/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "rust"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
22
55_Life/rust/README.md
Normal file
22
55_Life/rust/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Conway's Life
|
||||
|
||||
Original from David Ahl's _Basic Computer Games_, downloaded from http://www.vintage-basic.net/games.html.
|
||||
|
||||
Ported to Rust by Jon Fetter-Degges
|
||||
|
||||
Developed and tested on Rust 1.64.0
|
||||
|
||||
## How to Run
|
||||
|
||||
Install Rust using the instructions at [rust-lang.org](https://www.rust-lang.org/tools/install).
|
||||
|
||||
At a command or shell prompt in the `rust` subdirectory, enter `cargo run`.
|
||||
|
||||
## Differences from Original Behavior
|
||||
|
||||
* The simulation stops if all cells die.
|
||||
* `.` at the beginning of an input line is supported but optional.
|
||||
* Input of more than 66 columns is rejected. Input will automatically terminate after 20 rows. Beyond these bounds, the original
|
||||
implementation would have marked the board as invalid, and beyond 68 cols/24 rows it would have had an out of bounds array access.
|
||||
* The check for the string "DONE" at the end of input is case-independent.
|
||||
* The program pauses for half a second between each generation.
|
||||
275
55_Life/rust/src/main.rs
Normal file
275
55_Life/rust/src/main.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
// Rust implementation of the "Basic Computer Games" version of Conway's Life
|
||||
//
|
||||
// Jon Fetter-Degges
|
||||
// October 2022
|
||||
|
||||
// I am a Rust newbie. Corrections and suggestions are welcome.
|
||||
|
||||
use std::{cmp, fmt, io, thread, time};
|
||||
|
||||
// The BASIC implementation uses integers to represent the state of each cell: 1 is
|
||||
// alive, 2 is about to die, 3 is about to be born, 0 is dead. Here, we'll use an enum
|
||||
// instead.
|
||||
// Deriving Copy (which requires Clone) allows us to use this enum value in assignments,
|
||||
// and deriving Eq (or PartialEq) allows us to use the == operator. These need to be
|
||||
// explicitly specified because some enums may have associated data that makes copies and
|
||||
// comparisons more complicated or expensive.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum CellState {
|
||||
Empty,
|
||||
Alive,
|
||||
AboutToDie,
|
||||
AboutToBeBorn,
|
||||
}
|
||||
|
||||
// Support direct printing of the cell. In this program cells will only be Alive or Empty
|
||||
// when they are printed.
|
||||
impl fmt::Display for CellState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let rep = match *self {
|
||||
CellState::Empty => ' ',
|
||||
CellState::Alive => '*',
|
||||
CellState::AboutToDie => 'o',
|
||||
CellState::AboutToBeBorn => '.',
|
||||
};
|
||||
write!(f, "{}", rep)
|
||||
}
|
||||
}
|
||||
|
||||
// Following the BASIC implementation, we will bound the board at 24 rows x 70 columns.
|
||||
// The board is an array of CellState. Using an array of arrays gives us bounds checking
|
||||
// in both dimensions.
|
||||
const HEIGHT: usize = 24;
|
||||
const WIDTH: usize = 70;
|
||||
|
||||
struct Board {
|
||||
cells: [[CellState; WIDTH]; HEIGHT],
|
||||
min_row: usize,
|
||||
max_row: usize,
|
||||
min_col: usize,
|
||||
max_col: usize,
|
||||
population: usize,
|
||||
generation: usize,
|
||||
invalid: bool,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
fn new() -> Board {
|
||||
Board {
|
||||
cells: [[CellState::Empty; WIDTH]; HEIGHT],
|
||||
min_row: 0,
|
||||
max_row: 0,
|
||||
min_col: 0,
|
||||
max_col: 0,
|
||||
population: 0,
|
||||
generation: 0,
|
||||
invalid: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!(); println!(); println!();
|
||||
println!("{:33}{}", " ", "Life");
|
||||
println!("{:14}{}", " ", "Creative Computing Morristown, New Jersey");
|
||||
println!("Enter your pattern: ");
|
||||
let mut board = parse_pattern(get_pattern());
|
||||
loop {
|
||||
finish_cell_transitions(&mut board);
|
||||
print_board(&board);
|
||||
mark_cell_transitions(&mut board);
|
||||
if board.population == 0 {
|
||||
break; // this isn't in the original implementation but it seemed better than
|
||||
// spewing blank screens
|
||||
}
|
||||
delay();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_pattern() -> Vec<Vec<char>> {
|
||||
let max_line_len = WIDTH - 4;
|
||||
let max_line_count = HEIGHT - 4;
|
||||
let mut lines = Vec::new();
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
// read_line reads into the buffer (appending if it's not empty). It returns the
|
||||
// number of characters read, including the newline. This will be 0 on EOF.
|
||||
// unwrap() will panic and terminate the program if there is an error reading
|
||||
// from stdin. That's reasonable behavior in this case.
|
||||
let nread = io::stdin().read_line(&mut line).unwrap();
|
||||
let line = line.trim_end();
|
||||
if nread == 0 || line.eq_ignore_ascii_case("DONE") {
|
||||
return lines;
|
||||
}
|
||||
// Handle Unicode by converting the string to a vector of characters up front. We
|
||||
// do this here because we check the number of characters several times, so we
|
||||
// might as well just do the Unicode parsing once.
|
||||
let line = Vec::from_iter(line.chars());
|
||||
if line.len() > max_line_len {
|
||||
println!("Line too long - the maximum is {max_line_len} characters.");
|
||||
continue;
|
||||
}
|
||||
lines.push(line);
|
||||
if lines.len() == max_line_count {
|
||||
println!("Maximum line count reached. Starting simulation.");
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_pattern(rows: Vec<Vec<char>>) -> Board {
|
||||
// This function assumes that the input pattern in rows is in-bounds. If the pattern
|
||||
// is too large, this function will panic. get_pattern checks the size of the input,
|
||||
// so it is safe to call this function with its results.
|
||||
|
||||
let mut board = Board::new();
|
||||
|
||||
// The BASIC implementation puts the pattern roughly in the center of the board,
|
||||
// assuming that there are no blank rows at the beginning or end, or blanks entered
|
||||
// at the beginning or end of every row. It wouldn't be hard to check for that, but
|
||||
// for now we'll preserve the original behavior.
|
||||
let nrows = rows.len();
|
||||
// If rows is empty, the call to max will return None. The unwrap_or then provides a
|
||||
// default value
|
||||
let ncols = rows.iter().map(|l| l.len()).max().unwrap_or(0);
|
||||
|
||||
// The min and max values here are unsigned. If nrows >= 24 or ncols >= 68, these
|
||||
// assignments will panic - they do not wrap around unless we use a function with
|
||||
// that specific behavior. Again, we expect bounds checking on the input before this
|
||||
// function is called.
|
||||
board.min_row = 11 - nrows / 2;
|
||||
board.min_col = 33 - ncols / 2;
|
||||
board.max_row = board.min_row + nrows - 1;
|
||||
board.max_col = board.min_col + ncols - 1;
|
||||
|
||||
// Loop over the rows provided. enumerate() augments the iterator with an index.
|
||||
for (row_index, pattern) in rows.iter().enumerate() {
|
||||
let row = board.min_row + row_index;
|
||||
// Now loop over the non-empty cells in the current row. filter_map takes a
|
||||
// closure that returns an Option. If the Option is None, filter_map filters out
|
||||
// that entry from the for loop. If it's Some(x), filter_map executes the loop
|
||||
// body with the value x.
|
||||
for col in pattern.iter().enumerate().filter_map(|(col_index, chr)| {
|
||||
if *chr == ' ' || (*chr == '.' && col_index == 0) {
|
||||
None
|
||||
} else {
|
||||
Some(board.min_col + col_index)
|
||||
}
|
||||
}) {
|
||||
board.cells[row][col] = CellState::Alive;
|
||||
board.population += 1;
|
||||
}
|
||||
}
|
||||
|
||||
board
|
||||
}
|
||||
|
||||
fn finish_cell_transitions(board: &mut Board) {
|
||||
// In the BASIC implementation, this happens in the same loop that prints the board.
|
||||
// We're breaking it out to improve separation of concerns.
|
||||
let mut min_row = HEIGHT - 1;
|
||||
let mut max_row = 0usize;
|
||||
let mut min_col = WIDTH - 1;
|
||||
let mut max_col = 0usize;
|
||||
for row_index in board.min_row-1..=board.max_row+1 {
|
||||
let mut any_alive_this_row = false;
|
||||
for col_index in board.min_col-1..=board.max_col+1 {
|
||||
let cell = &mut board.cells[row_index][col_index];
|
||||
if *cell == CellState::AboutToBeBorn {
|
||||
*cell = CellState::Alive;
|
||||
board.population += 1;
|
||||
} else if *cell == CellState::AboutToDie {
|
||||
*cell = CellState::Empty;
|
||||
board.population -= 1;
|
||||
}
|
||||
if *cell == CellState::Alive {
|
||||
any_alive_this_row = true;
|
||||
min_col = cmp::min(min_col, col_index);
|
||||
max_col = cmp::max(max_col, col_index);
|
||||
}
|
||||
}
|
||||
if any_alive_this_row {
|
||||
min_row = cmp::min(min_row, row_index);
|
||||
max_row = cmp::max(max_row, row_index);
|
||||
}
|
||||
}
|
||||
// If anything is alive within two cells of the boundary, mark the board invalid and
|
||||
// clamp the bounds. We need a two-cell margin because we'll count neighbors on cells
|
||||
// one space outside the min/max, and when we count neighbors we go out by an
|
||||
// additional space.
|
||||
if min_row < 2 {
|
||||
min_row = 2;
|
||||
board.invalid = true;
|
||||
}
|
||||
if max_row > HEIGHT - 3 {
|
||||
max_row = HEIGHT - 3;
|
||||
board.invalid = true;
|
||||
}
|
||||
if min_col < 2 {
|
||||
min_col = 2;
|
||||
board.invalid = true;
|
||||
}
|
||||
if max_col > WIDTH - 3 {
|
||||
max_col = WIDTH - 3;
|
||||
board.invalid = true;
|
||||
}
|
||||
|
||||
board.min_row = min_row;
|
||||
board.max_row = max_row;
|
||||
board.min_col = min_col;
|
||||
board.max_col = max_col;
|
||||
}
|
||||
|
||||
fn print_board(board: &Board) {
|
||||
println!(); println!(); println!();
|
||||
print!("Generation: {} Population: {}", board.generation, board.population);
|
||||
if board.invalid {
|
||||
print!(" Invalid!");
|
||||
}
|
||||
println!();
|
||||
for row_index in 0..HEIGHT {
|
||||
for col_index in 0..WIDTH {
|
||||
// This print uses the Display implementation for cell_state, above.
|
||||
print!("{}", board.cells[row_index][col_index]);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
fn count_neighbors(board: &Board, row_index: usize, col_index: usize) -> i32 {
|
||||
// Simply loop over all the immediate neighbors of a cell. We assume that the row and
|
||||
// column indices are not on (or outside) the boundary of the arrays; if they are,
|
||||
// the function will panic instead of going out of bounds.
|
||||
let mut count = 0;
|
||||
for i in row_index-1..=row_index+1 {
|
||||
for j in col_index-1..=col_index+1 {
|
||||
if i == row_index && j == col_index {
|
||||
continue;
|
||||
}
|
||||
if board.cells[i][j] == CellState::Alive || board.cells[i][j] == CellState::AboutToDie {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn mark_cell_transitions(board: &mut Board) {
|
||||
for row_index in board.min_row-1..=board.max_row+1 {
|
||||
for col_index in board.min_col-1..=board.max_col+1 {
|
||||
let neighbors = count_neighbors(board, row_index, col_index);
|
||||
// Borrow a mutable reference to the array cell
|
||||
let this_cell_state = &mut board.cells[row_index][col_index];
|
||||
*this_cell_state = match *this_cell_state {
|
||||
CellState::Empty if neighbors == 3 => CellState::AboutToBeBorn,
|
||||
CellState::Alive if !(2..=3).contains(&neighbors) => CellState::AboutToDie,
|
||||
_ => *this_cell_state,
|
||||
}
|
||||
}
|
||||
}
|
||||
board.generation += 1;
|
||||
}
|
||||
|
||||
fn delay() {
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
}
|
||||
109
70_Poetry/csharp/Context.cs
Normal file
109
70_Poetry/csharp/Context.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
namespace Poetry;
|
||||
|
||||
internal class Context
|
||||
{
|
||||
private readonly IReadWrite _io;
|
||||
private readonly IRandom _random;
|
||||
private int _phraseNumber;
|
||||
private int _groupNumber;
|
||||
private bool _skipComma;
|
||||
private int _lineCount;
|
||||
private bool _useGroup2;
|
||||
private bool _atStartOfLine = true;
|
||||
|
||||
public Context(IReadWrite io, IRandom random)
|
||||
{
|
||||
_io = io;
|
||||
_random = random;
|
||||
}
|
||||
|
||||
public int PhraseNumber => Math.Max(_phraseNumber - 1, 0);
|
||||
|
||||
public int GroupNumber
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = _useGroup2 ? 2 : _groupNumber;
|
||||
_useGroup2 = false;
|
||||
return Math.Max(value - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public int PhraseCount { get; set; }
|
||||
public bool GroupNumberIsValid => _groupNumber < 5;
|
||||
|
||||
public void WritePhrase()
|
||||
{
|
||||
Phrase.GetPhrase(this).Write(_io, this);
|
||||
_atStartOfLine = false;
|
||||
}
|
||||
|
||||
public void MaybeWriteComma()
|
||||
{
|
||||
if (!_skipComma && _random.NextFloat() <= 0.19F && PhraseCount != 0)
|
||||
{
|
||||
_io.Write(",");
|
||||
PhraseCount = 2;
|
||||
}
|
||||
_skipComma = false;
|
||||
}
|
||||
|
||||
public void WriteSpaceOrNewLine()
|
||||
{
|
||||
if (_random.NextFloat() <= 0.65F)
|
||||
{
|
||||
_io.Write(" ");
|
||||
PhraseCount += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
EndLine();
|
||||
PhraseCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(IRandom random)
|
||||
{
|
||||
_phraseNumber = random.Next(1, 6);
|
||||
_groupNumber += 1;
|
||||
_lineCount += 1;
|
||||
}
|
||||
|
||||
public void MaybeIndent()
|
||||
{
|
||||
if (PhraseCount == 0 && _groupNumber % 2 == 0)
|
||||
{
|
||||
_io.Write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetGroup()
|
||||
{
|
||||
_groupNumber = 0;
|
||||
EndLine();
|
||||
}
|
||||
|
||||
public bool MaybeCompleteStanza()
|
||||
{
|
||||
if (_lineCount > 20)
|
||||
{
|
||||
_io.WriteLine();
|
||||
PhraseCount = _lineCount = 0;
|
||||
_useGroup2 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal string MaybeCapitalise(string text) =>
|
||||
_atStartOfLine ? (char.ToUpper(text[0]) + text[1..]) : text;
|
||||
|
||||
public void SkipNextComma() => _skipComma = true;
|
||||
|
||||
public void EndLine()
|
||||
{
|
||||
_io.WriteLine();
|
||||
_atStartOfLine = true;
|
||||
}
|
||||
}
|
||||
78
70_Poetry/csharp/Phrase.cs
Normal file
78
70_Poetry/csharp/Phrase.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace Poetry;
|
||||
|
||||
internal class Phrase
|
||||
{
|
||||
private readonly static Phrase[][] _phrases = new Phrase[][]
|
||||
{
|
||||
new Phrase[]
|
||||
{
|
||||
new("midnight dreary"),
|
||||
new("fiery eyes"),
|
||||
new("bird or fiend"),
|
||||
new("thing of evil"),
|
||||
new("prophet")
|
||||
},
|
||||
new Phrase[]
|
||||
{
|
||||
new("beguiling me", ctx => ctx.PhraseCount = 2),
|
||||
new("thrilled me"),
|
||||
new("still sitting....", ctx => ctx.SkipNextComma()),
|
||||
new("never flitting", ctx => ctx.PhraseCount = 2),
|
||||
new("burned")
|
||||
},
|
||||
new Phrase[]
|
||||
{
|
||||
new("and my soul"),
|
||||
new("darkness there"),
|
||||
new("shall be lifted"),
|
||||
new("quoth the raven"),
|
||||
new(ctx => ctx.PhraseCount != 0, "sign of parting")
|
||||
},
|
||||
new Phrase[]
|
||||
{
|
||||
new("nothing more"),
|
||||
new("yet again"),
|
||||
new("slowly creeping"),
|
||||
new("...evermore"),
|
||||
new("nevermore")
|
||||
}
|
||||
};
|
||||
|
||||
private readonly Predicate<Context> _condition;
|
||||
private readonly string _text;
|
||||
private readonly Action<Context> _update;
|
||||
|
||||
private Phrase(Predicate<Context> condition, string text)
|
||||
: this(condition, text, _ => { })
|
||||
{
|
||||
}
|
||||
|
||||
private Phrase(string text, Action<Context> update)
|
||||
: this(_ => true, text, update)
|
||||
{
|
||||
}
|
||||
|
||||
private Phrase(string text)
|
||||
: this(_ => true, text, _ => { })
|
||||
{
|
||||
}
|
||||
|
||||
private Phrase(Predicate<Context> condition, string text, Action<Context> update)
|
||||
{
|
||||
_condition = condition;
|
||||
_text = text;
|
||||
_update = update;
|
||||
}
|
||||
|
||||
public static Phrase GetPhrase(Context context) => _phrases[context.GroupNumber][context.PhraseNumber];
|
||||
|
||||
public void Write(IReadWrite io, Context context)
|
||||
{
|
||||
if (_condition.Invoke(context))
|
||||
{
|
||||
io.Write(context.MaybeCapitalise(_text));
|
||||
}
|
||||
|
||||
_update.Invoke(context);
|
||||
}
|
||||
}
|
||||
32
70_Poetry/csharp/Poem.cs
Normal file
32
70_Poetry/csharp/Poem.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using static Poetry.Resources.Resource;
|
||||
|
||||
namespace Poetry;
|
||||
|
||||
internal class Poem
|
||||
{
|
||||
internal static void Compose(IReadWrite io, IRandom random)
|
||||
{
|
||||
io.Write(Streams.Title);
|
||||
|
||||
var context = new Context(io, random);
|
||||
|
||||
while (true)
|
||||
{
|
||||
context.WritePhrase();
|
||||
context.MaybeWriteComma();
|
||||
context.WriteSpaceOrNewLine();
|
||||
|
||||
while (true)
|
||||
{
|
||||
context.Update(random);
|
||||
context.MaybeIndent();
|
||||
|
||||
if (context.GroupNumberIsValid) { break; }
|
||||
|
||||
context.ResetGroup();
|
||||
|
||||
if (context.MaybeCompleteStanza()) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,12 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources/*.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
5
70_Poetry/csharp/Program.cs
Normal file
5
70_Poetry/csharp/Program.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using Games.Common.IO;
|
||||
global using Games.Common.Randomness;
|
||||
global using Poetry;
|
||||
|
||||
Poem.Compose(new ConsoleIO(), new RandomNumberGenerator());
|
||||
16
70_Poetry/csharp/Resources/Resource.cs
Normal file
16
70_Poetry/csharp/Resources/Resource.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Poetry.Resources;
|
||||
|
||||
internal static class Resource
|
||||
{
|
||||
internal static class Streams
|
||||
{
|
||||
public static Stream Title => GetStream();
|
||||
}
|
||||
|
||||
private static Stream GetStream([CallerMemberName] string? name = null) =>
|
||||
Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt")
|
||||
?? throw new Exception($"Could not find embedded resource stream '{name}'.");
|
||||
}
|
||||
5
70_Poetry/csharp/Resources/Title.txt
Normal file
5
70_Poetry/csharp/Resources/Title.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Poetry
|
||||
Creative Computing Morristown, New Jersey
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
5 Y=RND(-1)
|
||||
6 REM FOR X = 1 TO 100
|
||||
7 REM PRINT RND(1);","
|
||||
8 REM NEXT X
|
||||
9 REM GOTO 999
|
||||
10 PRINT TAB(30);"POETRY"
|
||||
20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY"
|
||||
30 PRINT:PRINT:PRINT
|
||||
@@ -26,17 +31,21 @@
|
||||
133 PRINT "SLOWLY CREEPING";:GOTO 210
|
||||
134 PRINT "...EVERMORE";:GOTO 210
|
||||
135 PRINT "NEVERMORE";
|
||||
210 IF U=0 OR RND(1)>.19 THEN 212
|
||||
210 GOSUB 500 : IF U=0 OR X>.19 THEN 212
|
||||
211 PRINT ",";:U=2
|
||||
212 IF RND(1)>.65 THEN 214
|
||||
212 GOSUB 500 : IF X>.65 THEN 214
|
||||
213 PRINT " ";:U=U+1:GOTO 215
|
||||
214 PRINT : U=0
|
||||
215 I=INT(INT(10*RND(1))/2)+1
|
||||
215 GOSUB 500 : I=INT(INT(10*X)/2)+1
|
||||
220 J=J+1 : K=K+1
|
||||
225 REM PRINT "I=";I;"; J=";J;"; K=";K;"; U=";U
|
||||
230 IF U>0 OR INT(J/2)<>J/2 THEN 240
|
||||
235 PRINT " ";
|
||||
240 ON J GOTO 90,110,120,130,250
|
||||
250 J=0 : PRINT : IF K>20 THEN 270
|
||||
260 GOTO 215
|
||||
270 PRINT : U=0 : K=0 : GOTO 110
|
||||
500 X = RND(1)
|
||||
505 REM PRINT "#";X;"#"
|
||||
510 RETURN
|
||||
999 END
|
||||
|
||||
Reference in New Issue
Block a user