Skip to content

Instantly share code, notes, and snippets.

@alaindet
Created October 14, 2024 15:10
Show Gist options
  • Save alaindet/cd3ba83a9d1c3f97c2dbf6904567e50c to your computer and use it in GitHub Desktop.
Save alaindet/cd3ba83a9d1c3f97c2dbf6904567e50c to your computer and use it in GitHub Desktop.
[WIP] CLI RPG v1.0
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