diff --git a/00_Utilities/markdown_todo_rust/src/main.rs b/00_Utilities/markdown_todo_rust/src/main.rs index d1f49bf4..9df64567 100644 --- a/00_Utilities/markdown_todo_rust/src/main.rs +++ b/00_Utilities/markdown_todo_rust/src/main.rs @@ -1,4 +1,4 @@ -use std::ffi::{OsString, OsStr}; +use std::ffi::OsStr; use std::{fs, io}; use std::fs::metadata; use std::path::{Path, PathBuf}; @@ -8,8 +8,6 @@ use std::path::{Path, PathBuf}; * @author Anthony Rubick */ - - //DATA const ROOT_DIR: &str = "../../"; const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language name, second element is the file extension @@ -25,13 +23,19 @@ const LANGUAGES: [(&str,&str); 10] = [ //first element of tuple is the language ("vbnet", "vb") ]; const OUTPUT_PATH: &str = "../../todo.md"; -//const INGORE: [&str;5] = ["../../.git","../../.vscode","../../00_Utilities","../../buildJvm","../../node_modules"]; //folders to ignore fn main() { //DATA let mut root_folders:Vec; let mut output_string: String = String::new(); let format_game_first: bool; + let ingore: [PathBuf;5] = [ + PathBuf::from(r"../../.git"), + PathBuf::from(r"../../.github"), + PathBuf::from(r"../../00_Alternate_Languages"), + PathBuf::from(r"../../00_Utilities"), + PathBuf::from(r"../../00_Common"), + ]; //folders to ignore //print welcome message println!(" @@ -66,15 +70,10 @@ fn main() { //for all folders, search for the languages and extensions root_folders = root_folders.into_iter().filter(|path| { - match fs::read_dir(path) { - Err(why) => {println!("! {:?}", why.kind()); false}, - Ok(paths) => { - paths.into_iter().filter(|f| metadata(f.as_ref().unwrap().path()).unwrap().is_dir()) //filter to only folders - .filter_map( |path| path.ok() ) //extract only the DirEntries - .any(|f| LANGUAGES.iter().any(|tup| OsString::from(tup.1).eq_ignore_ascii_case(f.file_name()))) //filter out ones that don't contain folders with the language names - } - } + //not one of the ignored folders + !ingore.contains(path) }).collect(); + root_folders.sort(); //create todo list if format_game_first { diff --git a/21_Calendar/rust/Cargo.toml b/21_Calendar/rust/Cargo.toml new file mode 100644 index 00000000..1ec69633 --- /dev/null +++ b/21_Calendar/rust/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/21_Calendar/rust/README.md b/21_Calendar/rust/README.md new file mode 100644 index 00000000..e616424e --- /dev/null +++ b/21_Calendar/rust/README.md @@ -0,0 +1,3 @@ +Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) + +Conversion to [Rust](https://www.rust-lang.org/) by [Uğur Küpeli](https://github.com/ugurkupeli) \ No newline at end of file diff --git a/21_Calendar/rust/src/main.rs b/21_Calendar/rust/src/main.rs new file mode 100644 index 00000000..a279f95d --- /dev/null +++ b/21_Calendar/rust/src/main.rs @@ -0,0 +1,152 @@ +use std::io::stdin; + +const WIDTH: usize = 64; +const DAYS_WIDTH: usize = WIDTH / 8; +const MONTH_WIDTH: usize = WIDTH - (DAYS_WIDTH * 2); +const DAY_NUMS_WIDTH: usize = WIDTH / 7; + +const DAYS: [&str; 7] = [ + "SUNDAY", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", +]; + +fn main() { + println!("\n\t\t CALENDAR"); + println!("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n"); + + let (starting_day, leap_year) = prompt(); + let (months, total_days) = get_months_and_days(leap_year); + + let mut days_passed = 0; + let mut current_day_index = DAYS.iter().position(|d| *d == starting_day).unwrap(); + + for (month, days) in months { + print_header(month, days_passed, total_days - days_passed); + print_days(&mut current_day_index, days); + days_passed += days as u16; + println!("\n"); + } +} + +fn prompt() -> (String, bool) { + let mut day = String::new(); + + loop { + println!("\nFirst day of the year?"); + if let Ok(_) = stdin().read_line(&mut day) { + day = day.trim().to_uppercase(); + if DAYS.contains(&day.as_str()) { + break; + } else { + day.clear(); + } + } + } + + let mut leap = false; + + loop { + println!("Is this a leap year?"); + let mut input = String::new(); + if let Ok(_) = stdin().read_line(&mut input) { + match input.to_uppercase().trim() { + "Y" | "YES" => { + leap = true; + break; + } + "N" | "NO" => break, + _ => (), + } + } + } + + println!(); + (day, leap) +} + +fn get_months_and_days(leap_year: bool) -> (Vec<(String, u8)>, u16) { + let months = [ + "JANUARY", + "FEBUARY", + "MARCH", + "APRIL", + "MAY", + "JUNE", + "JULY", + "AUGUST", + "SEPTEMBER", + "OCTOBER", + "NOVEMBER", + "DECEMBER", + ]; + + let mut months_with_days = Vec::new(); + let mut total_days: u16 = 0; + + for (i, month) in months.iter().enumerate() { + let days = if i == 1 { + if leap_year { + 29u8 + } else { + 28 + } + } else if if i < 7 { (i % 2) == 0 } else { (i % 2) != 0 } { + 31 + } else { + 30 + }; + + total_days += days as u16; + months_with_days.push((month.to_string(), days)); + } + + (months_with_days, total_days) +} + +fn print_between(s: String, w: usize, star: bool) { + let s = format!(" {s} "); + if star { + print!("{:*^w$}", s); + return; + } + print!("{:^w$}", s); +} + +fn print_header(month: String, days_passed: u16, days_left: u16) { + print_between(days_passed.to_string(), DAYS_WIDTH, true); + print_between(month.to_string(), MONTH_WIDTH, true); + print_between(days_left.to_string(), DAYS_WIDTH, true); + println!(); + + for d in DAYS { + let d = d.chars().nth(0).unwrap(); + print_between(d.to_string(), DAY_NUMS_WIDTH, false); + } + println!(); + + println!("{:*>WIDTH$}", ""); +} + +fn print_days(current_day_index: &mut usize, days: u8) { + let mut current_date = 1u8; + + print!("{:>w$}", " ", w = DAY_NUMS_WIDTH * *current_day_index); + + for _ in 1..=days { + print_between(current_date.to_string(), DAY_NUMS_WIDTH, false); + + if ((*current_day_index + 1) % 7) == 0 { + *current_day_index = 0; + println!(); + } else { + *current_day_index += 1; + } + + current_date += 1; + } +} diff --git a/24_Chemist/README.md b/24_Chemist/README.md index b36f0b04..fc62425a 100644 --- a/24_Chemist/README.md +++ b/24_Chemist/README.md @@ -17,5 +17,7 @@ http://www.vintage-basic.net/games.html (please note any difficulties or challenges in porting here) +There is a type in the original Basic, "...DECIDE **WHO** MUCH WATER..." should be "DECIDE **HOW** MUCH WATER" + #### External Links - C: https://github.com/ericfischer/basic-computer-games/blob/main/24%20Chemist/c/chemist.c diff --git a/29_Craps/lua/README.md b/29_Craps/lua/README.md index c063f42f..aac11dd0 100644 --- a/29_Craps/lua/README.md +++ b/29_Craps/lua/README.md @@ -1,3 +1,17 @@ Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html) -Conversion to [Lua](https://www.lua.org/) +Conversion to [Lua](https://www.lua.org/) by Alex Conconi + +--- + +### Lua porting notes + +- The `craps_main` function contains the main game loop, which iteratively +plays craps rounds by calling `play_round` and tracks winnings and losings. +- Replaced the original routine that tries to scramble the random number +generator with a proper seed initializer in Lua: `math.randomseed(os.time())` +(as advised in the general porting notes). +- Added basic input validation to accept only positive integers for the +wager and the answer to the "If you want to play again print 5" question. +- "If you want to play again print 5 if not print 2" reads a bit odd but +we decided to leave it as is and stay true to the BASIC original version. \ No newline at end of file diff --git a/29_Craps/lua/craps.lua b/29_Craps/lua/craps.lua new file mode 100644 index 00000000..ddd1ce30 --- /dev/null +++ b/29_Craps/lua/craps.lua @@ -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() diff --git a/55_Life/rust/Cargo.toml b/55_Life/rust/Cargo.toml new file mode 100644 index 00000000..1ec69633 --- /dev/null +++ b/55_Life/rust/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rust" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/55_Life/rust/README.md b/55_Life/rust/README.md new file mode 100644 index 00000000..19f2d09d --- /dev/null +++ b/55_Life/rust/README.md @@ -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. diff --git a/55_Life/rust/src/main.rs b/55_Life/rust/src/main.rs new file mode 100644 index 00000000..50aa4927 --- /dev/null +++ b/55_Life/rust/src/main.rs @@ -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> { + 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>) -> 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)); +} diff --git a/70_Poetry/csharp/Context.cs b/70_Poetry/csharp/Context.cs new file mode 100644 index 00000000..a9297915 --- /dev/null +++ b/70_Poetry/csharp/Context.cs @@ -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; + } +} diff --git a/70_Poetry/csharp/Phrase.cs b/70_Poetry/csharp/Phrase.cs new file mode 100644 index 00000000..f70de30b --- /dev/null +++ b/70_Poetry/csharp/Phrase.cs @@ -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 _condition; + private readonly string _text; + private readonly Action _update; + + private Phrase(Predicate condition, string text) + : this(condition, text, _ => { }) + { + } + + private Phrase(string text, Action update) + : this(_ => true, text, update) + { + } + + private Phrase(string text) + : this(_ => true, text, _ => { }) + { + } + + private Phrase(Predicate condition, string text, Action update) + { + _condition = condition; + _text = text; + _update = update; + } + + public static Phrase GetPhrase(Context context) => _phrases[context.GroupNumber][context.PhraseNumber]; + + public void Write(IReadWrite io, Context context) + { + if (_condition.Invoke(context)) + { + io.Write(context.MaybeCapitalise(_text)); + } + + _update.Invoke(context); + } +} \ No newline at end of file diff --git a/70_Poetry/csharp/Poem.cs b/70_Poetry/csharp/Poem.cs new file mode 100644 index 00000000..fa3d5045 --- /dev/null +++ b/70_Poetry/csharp/Poem.cs @@ -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; } + } + } + } +} \ No newline at end of file diff --git a/70_Poetry/csharp/Poetry.csproj b/70_Poetry/csharp/Poetry.csproj index d3fe4757..3870320c 100644 --- a/70_Poetry/csharp/Poetry.csproj +++ b/70_Poetry/csharp/Poetry.csproj @@ -6,4 +6,12 @@ enable enable + + + + + + + + diff --git a/70_Poetry/csharp/Program.cs b/70_Poetry/csharp/Program.cs new file mode 100644 index 00000000..3a17d72c --- /dev/null +++ b/70_Poetry/csharp/Program.cs @@ -0,0 +1,5 @@ +global using Games.Common.IO; +global using Games.Common.Randomness; +global using Poetry; + +Poem.Compose(new ConsoleIO(), new RandomNumberGenerator()); diff --git a/70_Poetry/csharp/Resources/Resource.cs b/70_Poetry/csharp/Resources/Resource.cs new file mode 100644 index 00000000..b789f035 --- /dev/null +++ b/70_Poetry/csharp/Resources/Resource.cs @@ -0,0 +1,16 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Poetry.Resources; + +internal static class Resource +{ + internal static class Streams + { + public static Stream Title => GetStream(); + } + + private static Stream GetStream([CallerMemberName] string? name = null) => + Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt") + ?? throw new Exception($"Could not find embedded resource stream '{name}'."); +} \ No newline at end of file diff --git a/70_Poetry/csharp/Resources/Title.txt b/70_Poetry/csharp/Resources/Title.txt new file mode 100644 index 00000000..86161340 --- /dev/null +++ b/70_Poetry/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Poetry + Creative Computing Morristown, New Jersey + + + diff --git a/70_Poetry/poetry.bas b/70_Poetry/poetry.bas index 3661287d..7fa3a1c4 100644 --- a/70_Poetry/poetry.bas +++ b/70_Poetry/poetry.bas @@ -1,3 +1,8 @@ +5 Y=RND(-1) +6 REM FOR X = 1 TO 100 +7 REM PRINT RND(1);"," +8 REM NEXT X +9 REM GOTO 999 10 PRINT TAB(30);"POETRY" 20 PRINT TAB(15);"CREATIVE COMPUTING MORRISTOWN, NEW JERSEY" 30 PRINT:PRINT:PRINT @@ -26,17 +31,21 @@ 133 PRINT "SLOWLY CREEPING";:GOTO 210 134 PRINT "...EVERMORE";:GOTO 210 135 PRINT "NEVERMORE"; -210 IF U=0 OR RND(1)>.19 THEN 212 +210 GOSUB 500 : IF U=0 OR X>.19 THEN 212 211 PRINT ",";:U=2 -212 IF RND(1)>.65 THEN 214 +212 GOSUB 500 : IF X>.65 THEN 214 213 PRINT " ";:U=U+1:GOTO 215 214 PRINT : U=0 -215 I=INT(INT(10*RND(1))/2)+1 +215 GOSUB 500 : I=INT(INT(10*X)/2)+1 220 J=J+1 : K=K+1 +225 REM PRINT "I=";I;"; J=";J;"; K=";K;"; U=";U 230 IF U>0 OR INT(J/2)<>J/2 THEN 240 235 PRINT " "; 240 ON J GOTO 90,110,120,130,250 250 J=0 : PRINT : IF K>20 THEN 270 260 GOTO 215 270 PRINT : U=0 : K=0 : GOTO 110 +500 X = RND(1) +505 REM PRINT "#";X;"#" +510 RETURN 999 END