feat: add Console.readLine and Console.readInt, guessing game project

Add readLine and readInt operations to the Console effect for interactive
input. Create a number guessing game project demonstrating ADTs, pattern
matching, effects, and game state management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 17:58:00 -05:00
parent 44f88afcf8
commit 719bc77243
3 changed files with 231 additions and 1 deletions

View File

@@ -0,0 +1,203 @@
// Number Guessing Game
// The computer picks a random number, you try to guess it!
// Game configuration using ADTs
type Difficulty =
| Easy // 1-50, unlimited guesses
| Medium // 1-100, 10 guesses
| Hard // 1-200, 7 guesses
// GameConfig: minNum, maxNum, maxGuesses
type GameConfig =
| GameConfig(Int, Int, Option<Int>)
// GameState: secret, guesses, config
type GameState =
| GameState(Int, Int, GameConfig)
type GuessResult =
| TooLow
| TooHigh
| Correct
// Config helpers
fn getMinNum(config: GameConfig): Int =
match config { GameConfig(min, _, _) => min }
fn getMaxNum(config: GameConfig): Int =
match config { GameConfig(_, max, _) => max }
fn getMaxGuesses(config: GameConfig): Option<Int> =
match config { GameConfig(_, _, maxG) => maxG }
// State helpers
fn getSecret(state: GameState): Int =
match state { GameState(s, _, _) => s }
fn getGuessCount(state: GameState): Int =
match state { GameState(_, g, _) => g }
fn getConfig(state: GameState): GameConfig =
match state { GameState(_, _, c) => c }
fn difficultyConfig(d: Difficulty): GameConfig =
match d {
Easy => GameConfig(1, 50, None),
Medium => GameConfig(1, 100, Some(10)),
Hard => GameConfig(1, 200, Some(7))
}
fn difficultyName(d: Difficulty): String =
match d {
Easy => "Easy (1-50, unlimited guesses)",
Medium => "Medium (1-100, 10 guesses)",
Hard => "Hard (1-200, 7 guesses)"
}
// Core game logic (pure functions)
fn checkGuess(guess: Int, secret: Int): GuessResult =
if guess < secret then TooLow
else if guess > secret then TooHigh
else Correct
fn isGameOver(state: GameState, result: GuessResult): Bool =
match result {
Correct => true,
_ => match getMaxGuesses(getConfig(state)) {
Some(max) => getGuessCount(state) >= max,
None => false
}
}
fn guessesRemaining(state: GameState): Option<Int> =
match getMaxGuesses(getConfig(state)) {
Some(max) => Some(max - getGuessCount(state)),
None => None
}
// Game effects
fn printWelcome(): Unit with {Console} = {
Console.print("")
Console.print("========================================")
Console.print(" NUMBER GUESSING GAME")
Console.print("========================================")
Console.print("")
}
fn chooseDifficulty(): Difficulty with {Console} = {
Console.print("Choose difficulty:")
Console.print(" 1. " + difficultyName(Easy))
Console.print(" 2. " + difficultyName(Medium))
Console.print(" 3. " + difficultyName(Hard))
Console.print("")
Console.print("Enter choice (1-3): ")
let choice = Console.readInt()
match choice {
1 => Easy,
2 => Medium,
3 => Hard,
_ => {
Console.print("Invalid choice, defaulting to Medium")
Medium
}
}
}
fn initGame(difficulty: Difficulty): GameState with {Random} = {
let config = difficultyConfig(difficulty)
let secret = Random.int(getMinNum(config), getMaxNum(config))
GameState(secret, 0, config)
}
fn printGameStart(state: GameState): Unit with {Console} = {
let config = getConfig(state)
Console.print("")
Console.print("I'm thinking of a number between " +
toString(getMinNum(config)) + " and " +
toString(getMaxNum(config)) + "...")
match getMaxGuesses(config) {
Some(max) => Console.print("You have " + toString(max) + " guesses."),
None => Console.print("You have unlimited guesses.")
}
Console.print("")
}
fn getGuess(state: GameState): Int with {Console} = {
match guessesRemaining(state) {
Some(remaining) =>
Console.print("Guesses remaining: " + toString(remaining)),
None => ()
}
Console.print("Enter your guess: ")
Console.readInt()
}
fn printResult(result: GuessResult): Unit with {Console} =
match result {
TooLow => Console.print("Too low! Try higher."),
TooHigh => Console.print("Too high! Try lower."),
Correct => Console.print("")
}
fn printVictory(state: GameState): Unit with {Console} = {
Console.print("========================================")
Console.print(" CONGRATULATIONS! You got it!")
Console.print(" The number was: " + toString(getSecret(state)))
Console.print(" Guesses used: " + toString(getGuessCount(state)))
Console.print("========================================")
}
fn printDefeat(state: GameState): Unit with {Console} = {
Console.print("========================================")
Console.print(" GAME OVER - Out of guesses!")
Console.print(" The number was: " + toString(getSecret(state)))
Console.print("========================================")
}
// Main game loop
fn gameLoop(state: GameState): GameState with {Console} = {
let guess = getGuess(state)
let result = checkGuess(guess, getSecret(state))
let newState = GameState(getSecret(state), getGuessCount(state) + 1, getConfig(state))
printResult(result)
if isGameOver(newState, result) then {
match result {
Correct => printVictory(newState),
_ => printDefeat(newState)
}
newState
} else {
gameLoop(newState)
}
}
fn askPlayAgain(): Bool with {Console} = {
Console.print("")
Console.print("Play again? (1 = yes, 2 = no): ")
let choice = Console.readInt()
choice == 1
}
fn playGame(): Unit with {Console, Random} = {
let difficulty = chooseDifficulty()
let state = initGame(difficulty)
printGameStart(state)
let finalState = gameLoop(state)
()
}
fn mainLoop(): Unit with {Console, Random} = {
playGame()
if askPlayAgain() then mainLoop()
else Console.print("Thanks for playing! Goodbye!")
}
fn main(): Unit with {Console, Random} = {
printWelcome()
mainLoop()
}
let output = run main() with {}

View File

@@ -2880,7 +2880,7 @@ impl Interpreter {
Ok(Value::Unit)
}
}
("Console", "read") => {
("Console", "read") | ("Console", "readLine") => {
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
@@ -2890,6 +2890,23 @@ impl Interpreter {
})?;
Ok(Value::String(input.trim().to_string()))
}
("Console", "readInt") => {
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.map_err(|e| RuntimeError {
message: format!("Failed to read input: {}", e),
span: None,
})?;
let trimmed = input.trim();
match trimmed.parse::<i64>() {
Ok(n) => Ok(Value::Int(n)),
Err(_) => Err(RuntimeError {
message: format!("Invalid integer: '{}'", trimmed),
span: None,
}),
}
}
("Fail", "fail") => {
let msg = request
.args

View File

@@ -780,6 +780,16 @@ impl TypeEnv {
params: Vec::new(),
return_type: Type::String,
},
EffectOpDef {
name: "readLine".to_string(),
params: Vec::new(),
return_type: Type::String,
},
EffectOpDef {
name: "readInt".to_string(),
params: Vec::new(),
return_type: Type::Int,
},
],
},
);