Created
October 14, 2024 15:10
-
-
Save alaindet/cd3ba83a9d1c3f97c2dbf6904567e50c to your computer and use it in GitHub Desktop.
[WIP] CLI RPG v1.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createInterface } from "node:readline"; | |
const DIRECTION_AHEAD = "ahead"; | |
const DIRECTION_LEFT = "left"; | |
const DIRECTION_RIGHT = "right"; | |
const DIRECTION_BACK = "back"; | |
const MOVE_ATTACK = "attack"; | |
const MOVE_FLEE = "flee"; | |
const VILLAGE_TAVERN = "tavern"; | |
const VILLAGE_MARKET = "market"; | |
const MARKET_SWORD = "sword"; | |
const MARKET_ARMOR = "armor"; | |
const MARKET_POTION = "potion"; | |
const ITEM_POTION = "potion"; | |
main(); // <-- Start here! | |
function main() { | |
const inputReader = createInputReader(); | |
playRpgGame(inputReader); | |
inputReader.close(); | |
} | |
async function playRpgGame(inputReader) { | |
// Start game | |
welcome(); | |
let state = initializeGameState(); | |
// Ask player name | |
const playerName = await askPlayerName(inputReader); | |
state.playerName = playerName; | |
// Play turns | |
while (state.playing) { | |
state = await exploreArea(inputReader, state); | |
// Check game over? | |
if (!state.playing) { | |
const playAgain = await askPlayAgain(inputReader, state); | |
if (playAgain) { | |
state = initializeGameState(); | |
state.playerName = playerName; | |
continue; | |
} | |
return goodbye(); | |
} | |
const continuePlaying = await askContinuePlaying(inputReader, state); | |
if (!continuePlaying) { | |
return goodbye(); | |
} | |
} | |
} | |
function initializeGameState() { | |
return { | |
playing: true, | |
playerName: "", | |
playerHealth: 100, | |
playerArmor: 0, | |
playerAttack: 10, | |
playerBag: {}, | |
enemyName: null, | |
enemyHealth: null, | |
enemyAttack: 13, | |
}; | |
} | |
function welcome() { | |
const title = "Welcome to RPG v1.0"; | |
const separator = "=".repeat(title.length); | |
console.log(`${title}\n${separator}\n`); | |
} | |
function goodbye() { | |
console.log("Thanks for playing RPG v1.0"); | |
} | |
async function exploreArea(inputReader, state) { | |
console.log("You're roaming around alone, in a dark forest..."); | |
const chosenPath = await askPathToFollow(inputReader); | |
switch (chosenPath) { | |
case DIRECTION_AHEAD: | |
case DIRECTION_RIGHT: { | |
const chance = getRandomInteger(0, 9); | |
if (chance < 4) { | |
return encounterGoblin(inputReader, state); | |
} | |
if (chance > 5) { | |
return encounterWerewolf(inputReader, state); | |
} | |
return encounterVillage(); | |
} | |
case DIRECTION_LEFT: | |
case DIRECTION_BACK: { | |
const chance = getRandomInteger(0, 9); | |
if (chance >= 2) { | |
return runAway(state); | |
} | |
return encounterVillage(); | |
} | |
} | |
} | |
function runAway(state) { | |
console.log("You escaped. Game over"); | |
return state; | |
} | |
function encounterGoblin(inputReader, state) { | |
state.enemyName = "Goblin"; | |
state.enemyHealth = 40; | |
state.enemyAttack = 13; | |
return combat(inputReader, state); | |
} | |
function encounterWerewolf(inputReader, state) { | |
state.enemyName = "Werewolf"; | |
state.enemyHealth = 50; | |
state.enemyAttack = 13; | |
return combat(inputReader, state); | |
} | |
async function encounterVillage(inputReader, state) { | |
const villageAction = await askVillageAction(inputReader); | |
switch (villageAction) { | |
case VILLAGE_TAVERN: | |
console.log("Resting is good for you. Player health replenished"); | |
state.playerHealth = 100; | |
return state; | |
case VILLAGE_MARKET: | |
return visitMarket(inputReader, state); | |
} | |
} | |
async function visitMarket(inputReader, state) { | |
console.log("You enter the local market"); | |
const marketAction = await askMarketAction(inputReader); | |
switch (marketAction) { | |
case MARKET_SWORD: | |
state.playerAttack += 5; | |
return state; | |
case MARKET_ARMOR: | |
state.playerArmor += 5; | |
return state; | |
case MARKET_POTION: | |
addToPlayerBag(state, ITEM_POTION); | |
return state; | |
} | |
} | |
async function combat(inputReader, state) { | |
while (state.playerHealth >= 0 && state.enemyHealth >= 0) { | |
const playerMove = await askPlayerMove(inputReader); | |
switch (playerMove) { | |
case MOVE_ATTACK: | |
while (state.playerHealth > 0 || state.enemyHealth > 0) { | |
// Player shot first | |
state.enemyHealth -= state.playerAttack; | |
console.log(`You attacked ${state.enemyName}!`); | |
// The enemy strikes back | |
if (state.enemyHealth > 0) { | |
state.playerHealth -= state.enemyAttack - state.playerArmor; | |
console.log( | |
`The ${state.enemyName} hit you! Your remaining health is ${state.playerHealth}` | |
); | |
} | |
if (state.enemyHealth === 0) { | |
console.log(`You defeated the ${state.enemyName}!`); | |
state = addToPlayerBag(state, ITEM_POTION); | |
return state; | |
} | |
if (state.playerHealth === 0) { | |
console.log(`Wasted! (You died)`); | |
state.playing = false; | |
return state; | |
} | |
} | |
case MOVE_FLEE: | |
console.log("You fled from combat!"); | |
return exploreArea(inputReader, state); | |
} | |
} | |
} | |
function addToPlayerBag(state, itemName) { | |
if (itemName in state.playerBag) { | |
state.playerBag[itemName] += 1; | |
} else { | |
state.playerBag[itemName] = 1; | |
} | |
return state; | |
} | |
async function askPlayerName(inputReader) { | |
while (true) { | |
const playerName = await inputReader.read("What's your name?"); | |
// TODO: Remove | |
console.log("[DEBUG] playerName", playerName); | |
if (playerName === "") { | |
console.log("Invalid user name"); | |
continue; | |
} | |
if (playerName.toLowerCase() === "fabrizio") { | |
console.log("Not you, Fabrizio!"); | |
continue; | |
} | |
return playerName; | |
} | |
} | |
async function askBoolean(inputReader, message) { | |
while (true) { | |
const answer = await inputReader.read(`${message} (Y)es, (N)o`); | |
if (answer === "y" || answer === "Y") { | |
return true; | |
} | |
if (answer === "n" || answer === "N") { | |
return false; | |
} | |
console.log("Invalid answer given"); | |
} | |
} | |
function askPlayAgain(inputReader) { | |
return askBoolean(inputReader, "Do you want to play again?"); | |
} | |
function askContinuePlaying(inputReader) { | |
return askBoolean(inputReader, "Do you want to continue your journey?"); | |
} | |
function askPathToFollow(inputReader) { | |
return askToChoose( | |
inputReader, | |
"Where do you want to go? (A)head, (L)eft, (R)ight, (B)ack", | |
[DIRECTION_AHEAD, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_BACK] | |
); | |
} | |
function askPlayerMove(inputReader) { | |
return askToChoose(inputReader, "What do you want to do? (A)ttack, (F)lee", [ | |
MOVE_ATTACK, | |
MOVE_FLEE, | |
]); | |
} | |
function askVillageAction(inputReader) { | |
return askToChoose( | |
inputReader, | |
'A jolly villager greets you: "Aye traveler, would you like to rest at the (T)avern, or maybe visit our local (M)arket?"', | |
[VILLAGE_TAVERN, VILLAGE_MARKET] | |
); | |
} | |
function askMarketAction(inputReader) { | |
return askToChoose( | |
inputReader, | |
"What would you like to buy? (S)word, (A)rmor, (P)otion", | |
[MARKET_SWORD, MARKET_ARMOR, MARKET_POTION] | |
); | |
} | |
async function askToChoose(inputReader, question, choices) { | |
const choicesMap = new Map( | |
choices.map((choice) => { | |
const key = choice[0].toLowerCase(); | |
return [key, choice]; | |
}) | |
); | |
while (true) { | |
const action = await inputReader.read(question).toLowerCase(); | |
const choice = choicesMap.get(action); | |
if (choice !== null) { | |
return choice; | |
} | |
console.log("Invalid choice given"); | |
} | |
} | |
function getRandomInteger(a, b) { | |
return a + Math.floor(Math.random() * (b - a + 1)); | |
} | |
function createInputReader() { | |
const rl = createInterface({ | |
input: process.stdin, | |
output: process.stdout, | |
}); | |
function read(questionMessage) { | |
const prompt = questionMessage + "\n> "; | |
return new Promise((done) => { | |
rl.question(prompt, (input) => { | |
if (!input) { | |
throw new Error("No input provided"); | |
} | |
done(String(input)); | |
}); | |
}); | |
} | |
function close() { | |
rl.close(); | |
} | |
return { | |
read, | |
close, | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment