AI Dungeon Master - Procedural Adventure - xelsed.ai

This is a fully AI-powered text adventure game where OpenAI acts as a Dungeon Master, generating narrative, tracking game state, and creating meaningful consequences. The canvas procedurally draws dynamic scenes (forests, caves, castles, villages, rivers) based on the AI's descriptions, while the game manages health, inventory, and story history through an interactive choice-based system.

πŸŽ“ Concepts You'll Learn

API integrationAsynchronous functionsGame state managementProcedural drawingDOM manipulationSpeech synthesisConditional renderingJSON parsingCanvas gradientsEvent handling

πŸ”„ Code Flow

Code flow showing setup, draw, windowresized, startgame, handlechoice, callopenai, updategame, updateui, restartgame, speak, drawscene, drawtimeofday, setgradient, drawweather, drawforest, drawcave, drawcastle, drawvillage, drawriver, drawelements, getapikey

πŸ’‘ Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> canvas-creation[Canvas Sizing] setup --> dom-selection[DOM Element Selection] setup --> speech-init[Speech Synthesis Initialization] setup --> draw[draw loop] click setup href "#fn-setup" click canvas-creation href "#sub-canvas-creation" click dom-selection href "#sub-dom-selection" click speech-init href "#sub-speech-init" draw --> time-draw[Time of Day Background] draw --> setting-switch[Setting Router] draw --> weather-draw[Weather Effects] draw --> elements-draw[Scene Elements] draw --> drawscene[drawScene] click draw href "#fn-draw" click time-draw href "#sub-time-draw" click setting-switch href "#sub-setting-switch" click weather-draw href "#sub-weather-draw" click elements-draw href "#sub-elements-draw" click drawscene href "#fn-drawscene" drawscene --> drawtimeofday[drawTimeOfDay] drawscene --> drawweather[drawWeather] drawscene --> drawforest[drawForest] drawscene --> drawcave[drawCave] drawscene --> drawcastle[drawCastle] drawscene --> drawvillage[drawVillage] drawscene --> drawriver[drawRiver] drawscene --> drawelements[drawElements] click drawtimeofday href "#fn-drawtimeofday" click drawweather href "#fn-drawweather" click drawforest href "#fn-drawforest" click drawcave href "#fn-drawcave" click drawcastle href "#fn-drawcastle" click drawvillage href "#fn-drawvillage" click drawriver href "#fn-drawriver" click drawelements href "#fn-drawelements" windowresized --> draw click windowresized href "#fn-windowresized" startgame --> state-reset[Game State Reset] startgame --> loading-indicator[Show Loading Spinner] startgame --> callopenai[callOpenAI] click startgame href "#fn-startgame" click state-reset href "#sub-state-reset" click loading-indicator href "#sub-loading-indicator" click callopenai href "#fn-callopenai" callopenai --> api-fetch[API Request] callopenai --> error-handling[Response Status Check] callopenai --> json-parsing[JSON Parse Error Handling] callopenai --> finally-block[Hide Loading Spinner] click api-fetch href "#sub-api-fetch" click error-handling href "#sub-error-handling" click json-parsing href "#sub-json-parsing" click finally-block href "#sub-finally-block" updategame --> health-update[Health Adjustment] updategame --> location-update[Location Capitalization] updategame --> item-check[New Item Handling] updategame --> game-end-check[Game Over/Victory Check] updategame --> health-calc[Health Percentage Calculation] click updategame href "#fn-updategame" click health-update href "#sub-health-update" click location-update href "#sub-location-update" click item-check href "#sub-item-check" click game-end-check href "#sub-game-end-check" click health-calc href "#sub-health-calc" updateui --> inventory-loop[Inventory Display Loop] updateui --> choices-loop[Choice Buttons Loop] updateui --> game-state-conditional[Display Choices Only If Game Active] click updateui href "#fn-updateui" click inventory-loop href "#sub-inventory-loop" click choices-loop href "#sub-choices-loop" click game-state-conditional href "#sub-game-state-conditional" handlechoice --> game-state-check[Game State Guard] handlechoice --> history-push[Record Player Choice] handlechoice --> prompt-construction[Dynamic Prompt Building] click handlechoice href "#fn-handlechoice" click game-state-check href "#sub-game-state-check" click history-push href "#sub-history-push" click prompt-construction href "#sub-prompt-construction" speak --> voice-check[Voice Availability Check] click speak href "#fn-speak" click voice-check href "#sub-voice-check" getapikey --> base64-decode[Base64 Decoding] getapikey --> xor-cipher[XOR Decryption] click getapikey href "#fn-getapikey" click base64-decode href "#sub-base64-decode" click xor-cipher href "#sub-xor-cipher"

πŸ“ Code Breakdown

setup()

setup() runs once when the sketch starts. It initializes the canvas, caches DOM elements for performance, sets up speech synthesis, and starts the game. Caching DOM elements (storing them in variables) is important because querying the DOM repeatedly is slow.

function setup() {
  const gameContainer = select('#game-container');
  let canvasWidth = min(windowWidth * 0.9, 1400 - 30);
  let canvasHeight = min(windowHeight * 0.5, 900 * 0.5);
  let canvas = createCanvas(canvasWidth, canvasHeight);
  canvas.parent('game-container');

  healthBarDiv = select('#health-bar');
  healthTextSpan = select('#health-text');
  locationTextDiv = select('#location-text');
  narrativeTextP = select('#narrative-text');
  choicesContainer = select('#choices-container');
  inventoryListUl = select('#inventory-list');
  loadingOverlay = select('#loading-overlay');
  gameOverScreen = select('#game-over-screen');
  victoryScreen = select('#victory-screen');
  restartButton = select('#restart-button');

  restartButton.mousePressed(restartGame);

  synth = window.speechSynthesis;
  synth.onvoiceschanged = () => {
    voice = synth.getVoices().find(v => v.lang === 'en-US' && v.name.includes('Male')) || synth.getVoices().find(v => v.lang === 'en-US');
  };
  if (synth.getVoices().length > 0) {
    voice = synth.getVoices().find(v => v.lang === 'en-US' && v.name.includes('Male')) || synth.getVoices().find(v => v.lang === 'en-US');
  }

  updateUI();
  startGame();
}

πŸ”§ Subcomponents:

calculation Canvas Sizing let canvasWidth = min(windowWidth * 0.9, 1400 - 30);

Calculates responsive canvas width, taking 90% of window width but capping at 1370px to fit container

assignment DOM Element Selection healthBarDiv = select('#health-bar');

Caches references to HTML elements for efficient access throughout the sketch

conditional Speech Synthesis Initialization if (synth.getVoices().length > 0)

Checks if voices are pre-loaded and selects an English male voice for narration

Line by Line:

const gameContainer = select('#game-container');
Uses p5.js select() to get reference to the HTML element with id 'game-container' for responsive sizing calculations
let canvasWidth = min(windowWidth * 0.9, 1400 - 30);
Calculates canvas width as 90% of window width, but never exceeds 1370px (1400 minus 30px padding)
let canvas = createCanvas(canvasWidth, canvasHeight);
Creates the p5.js canvas with calculated dimensions for drawing the procedural scenes
canvas.parent('game-container');
Inserts the canvas element into the HTML container with id 'game-container'
healthBarDiv = select('#health-bar');
Stores reference to health bar DOM element so it can be updated without re-querying the DOM
restartButton.mousePressed(restartGame);
Attaches a click event listener to the restart button that calls the restartGame function
synth = window.speechSynthesis;
Gets the browser's built-in speech synthesis API for text-to-speech narration
voice = synth.getVoices().find(v => v.lang === 'en-US' && v.name.includes('Male'));
Searches available voices for an English US male voice, or falls back to any English voice if not found
updateUI();
Initializes the user interface with current game state (health bar, inventory, location)
startGame();
Begins the game by resetting state and making the first OpenAI API call to generate the opening narrative

draw()

draw() runs 60 times per second (default frame rate). In this sketch, it simply calls drawScene() to render the procedural environment. The actual game logic happens in async functions that respond to user input.

function draw() {
  drawScene();
}

Line by Line:

drawScene();
Calls the main scene drawing function which renders the background, setting, weather, and elements based on current game state

windowResized()

windowResized() is called automatically by p5.js whenever the browser window is resized. This ensures the game stays responsive on different screen sizes.

function windowResized() {
  const gameContainer = select('#game-container');
  let canvasWidth = min(windowWidth * 0.9, gameContainer.width - 30);
  let canvasHeight = min(windowHeight * 0.5, gameContainer.height * 0.5);
  resizeCanvas(canvasWidth, canvasHeight);
}

Line by Line:

let canvasWidth = min(windowWidth * 0.9, gameContainer.width - 30);
Recalculates canvas width when window is resized, maintaining responsive sizing
resizeCanvas(canvasWidth, canvasHeight);
p5.js function that resizes the canvas to new dimensions without losing the drawing

startGame()

startGame() is an async function that resets the game and makes the first API call. The 'async' keyword allows it to use 'await', which pauses execution until the API responds. This is crucial for handling network requests without freezing the game.

async function startGame() {
  gameState = {
    health: 100,
    inventory: [],
    location: 'Unknown',
    storyHistory: [],
    currentScene: {
      setting: 'forest',
      timeOfDay: 'day',
      weather: 'clear',
      elements: []
    }
  };
  updateUI();
  gameOverScreen.style('display', 'none');
  victoryScreen.style('display', 'none');

  const promptMessage = 'Start a new fantasy adventure. The hero wakes up in an unknown place. Generate JSON response as described.';
  await callOpenAI(promptMessage);
}

πŸ”§ Subcomponents:

assignment Game State Reset gameState = { health: 100, inventory: [], ... }

Resets all game variables to initial values for a fresh game

assignment Hide End Screens gameOverScreen.style('display', 'none');

Hides game over and victory screens so the game can be played

Line by Line:

async function startGame() {
Declares an async function so it can use 'await' to wait for OpenAI API responses
gameState = { health: 100, inventory: [], ... }
Resets the entire game state object to initial values: full health, empty inventory, unknown location
updateUI();
Updates all UI elements to reflect the reset game state
gameOverScreen.style('display', 'none');
Hides the game over screen by setting its CSS display property to 'none'
const promptMessage = 'Start a new fantasy adventure...';
Creates the initial prompt that tells OpenAI to begin a new adventure story
await callOpenAI(promptMessage);
Calls the OpenAI API with the prompt and waits for the response before continuing

handleChoice(choiceText)

handleChoice() is called when a player clicks a choice button. It records the choice in history and sends a detailed prompt to OpenAI that includes the entire game state and story. This allows the AI to make decisions that are consistent with previous events and maintain narrative coherence.

async function handleChoice(choiceText) {
  if (gameState.gameOver || gameState.victory) return;

  gameState.storyHistory.push(`Player chose: "${choiceText}"`);

  const promptMessage = `You are a creative Dungeon Master. Current state: ${JSON.stringify(gameState)}. Story so far: ${gameState.storyHistory.join('\n')}. Player chose: "${choiceText}". Generate JSON response: {narrative:string (2-3 sentences describing what happens), scene:{setting:forest/cave/castle/village/river, timeOfDay:day/night/dawn/dusk, weather:clear/rain/fog/storm, elements:[tree/rock/chest/monster/npc/fire/water]}, choices:[{id:1,text:string},{id:2,text:string},{id:3,text:string}], healthChange:number, newItem:string or null, gameOver:boolean, victory:boolean}`;

  await callOpenAI(promptMessage);
}

πŸ”§ Subcomponents:

conditional Game State Guard if (gameState.gameOver || gameState.victory) return;

Prevents processing new choices if the game has ended

assignment Record Player Choice gameState.storyHistory.push(`Player chose: "${choiceText}"`);

Adds the player's choice to the story history so OpenAI can maintain narrative continuity

calculation Dynamic Prompt Building const promptMessage = `You are a creative Dungeon Master...`;

Constructs a detailed prompt that includes full game state and story history for context-aware AI responses

Line by Line:

if (gameState.gameOver || gameState.victory) return;
Early exit: if the game is over or won, ignore the choice and return immediately
gameState.storyHistory.push(`Player chose: "${choiceText}"`);
Records the player's choice in the story history array so the AI can reference all previous decisions
const promptMessage = `You are a creative Dungeon Master...`;
Creates a detailed prompt that includes the current game state (as JSON), full story history, and the player's choice, so OpenAI has complete context
await callOpenAI(promptMessage);
Sends the prompt to OpenAI and waits for the response, which will update the game state

callOpenAI(promptMessage)

callOpenAI() is the core function that communicates with OpenAI's API. It uses async/await to handle the asynchronous network request, includes comprehensive error handling for network errors and JSON parsing failures, and uses a try-catch-finally pattern to ensure the loading indicator is always hidden. The response_format field is crucialβ€”it tells OpenAI to return valid JSON.

async function callOpenAI(promptMessage) {
  loadingOverlay.addClass('visible');
  try {
    const apiKey = getApiKey();
    const response = await fetch(OPENAI_API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${apiKey}`
      },
      body: JSON.stringify({
        model: OPENAI_MODEL,
        messages: [
          {"role": "system", "content": "You are a creative Dungeon Master. Your responses must be valid JSON as described. Maintain narrative coherence and player agency. Ensure choices are distinct and move the story forward."},
          {"role": "user", "content": promptMessage}
        ],
        response_format: { "type": "json_object" },
        temperature: OPENAI_TEMP,
        max_tokens: OPENAI_MAX_TOKENS
      })
    });

    if (!response.ok) {
      const errorData = await response.json();
      console.error('OpenAI API Error:', response.status, errorData);
      narrativeTextP.html(`An error occurred: ${errorData.error.message || response.statusText}. Please try again.`);
      speak('An error occurred. Please try again.');
      return;
    }

    const data = await response.json();
    const jsonString = data.choices[0].message.content;

    let parsedResponse;
    try {
      parsedResponse = JSON.parse(jsonString);
    } catch (parseError) {
      console.error('Failed to parse OpenAI JSON response:', parseError, jsonString);
      narrativeTextP.html('OpenAI returned invalid JSON. Trying again...');
      speak('The story is unclear. Trying again.');
      await callOpenAI(promptMessage);
      return;
    }

    updateGame(parsedResponse);

  } catch (error) {
    console.error('Network or other error:', error);
    narrativeTextP.html(`A network error occurred: ${error.message}. Please check your connection.`);
    speak('A network error occurred. Please check your connection.');
  } finally {
    loadingOverlay.removeClass('visible');
  }
}

πŸ”§ Subcomponents:

assignment Show Loading Spinner loadingOverlay.addClass('visible');

Displays a loading indicator to show the user that the API is processing

calculation API Request const response = await fetch(OPENAI_API_URL, { ... });

Makes an HTTP POST request to OpenAI's API with the prompt and game state

conditional Response Status Check if (!response.ok) { ... }

Checks if the API request was successful; if not, displays error message

conditional JSON Parse Error Handling try { parsedResponse = JSON.parse(jsonString); } catch (parseError) { ... }

Safely parses the JSON response and retries if parsing fails

assignment Hide Loading Spinner loadingOverlay.removeClass('visible');

Always hides the loading indicator, whether the request succeeded or failed

Line by Line:

loadingOverlay.addClass('visible');
Shows a loading spinner by adding the 'visible' CSS class to indicate the API is processing
const apiKey = getApiKey();
Decodes the API key from the encoded string using the getApiKey() function
const response = await fetch(OPENAI_API_URL, { ... });
Makes an async HTTP POST request to OpenAI's API endpoint with headers and body containing the prompt
headers: { 'Authorization': `Bearer ${apiKey}` }
Includes the API key in the Authorization header so OpenAI knows which account to charge
response_format: { "type": "json_object" }
Tells OpenAI to format its response as valid JSON, ensuring parseable output
if (!response.ok) { ... }
Checks if the HTTP status code indicates success (200-299); if not, displays error and returns
const data = await response.json();
Parses the HTTP response body as JSON to extract the message content
const jsonString = data.choices[0].message.content;
Extracts the actual text response from OpenAI's nested response structure
parsedResponse = JSON.parse(jsonString);
Parses the JSON string into a JavaScript object that can be used to update game state
updateGame(parsedResponse);
Passes the parsed response to updateGame() to apply the AI's narrative and game changes
} finally { loadingOverlay.removeClass('visible'); }
The finally block always executes, hiding the loading spinner whether the request succeeded or failed

updateGame(response)

updateGame() applies the OpenAI response to the game state. It updates health (with bounds checking), location, scene, inventory, and narrative text. It also handles game-ending conditions and uses text-to-speech to narrate the story. This function is the bridge between the AI's response and the player's experience.

function updateGame(response) {
  gameState.health = constrain(gameState.health + response.healthChange, 0, MAX_HEALTH);

  gameState.location = response.scene.setting.charAt(0).toUpperCase() + response.scene.setting.slice(1);
  gameState.currentScene = response.scene;

  gameState.storyHistory.push(response.narrative);

  if (response.newItem && !gameState.inventory.includes(response.newItem)) {
    gameState.inventory.push(response.newItem);
    narrativeTextP.html(`${response.narrative}<br><br>You found a ${response.newItem}!`);
    speak(`${response.narrative}. You found a ${response.newItem}!`);
  } else {
    narrativeTextP.html(response.narrative);
    speak(response.narrative);
  }

  updateUI(response.choices);

  if (gameState.health <= 0 || response.gameOver) {
    gameState.gameOver = true;
    speak('Game Over!');
    gameOverScreen.style('display', 'flex');
  } else if (response.victory) {
    gameState.victory = true;
    speak('You Win!');
    victoryScreen.style('display', 'flex');
  }
}

πŸ”§ Subcomponents:

calculation Health Adjustment gameState.health = constrain(gameState.health + response.healthChange, 0, MAX_HEALTH);

Updates health by adding the healthChange value, keeping it between 0 and 100

calculation Location Capitalization gameState.location = response.scene.setting.charAt(0).toUpperCase() + response.scene.setting.slice(1);

Capitalizes the first letter of the setting (e.g., 'forest' becomes 'Forest')

conditional New Item Handling if (response.newItem && !gameState.inventory.includes(response.newItem))

Checks if there's a new item and it's not already in inventory before adding it

conditional Game Over/Victory Check if (gameState.health <= 0 || response.gameOver)

Determines if the game should end due to death or explicit game over condition

Line by Line:

gameState.health = constrain(gameState.health + response.healthChange, 0, MAX_HEALTH);
Updates health by adding the healthChange value from OpenAI response, using constrain() to keep it between 0 and 100
gameState.location = response.scene.setting.charAt(0).toUpperCase() + response.scene.setting.slice(1);
Capitalizes the first letter of the setting name: charAt(0) gets first char, toUpperCase() capitalizes it, slice(1) gets remaining chars
gameState.currentScene = response.scene;
Updates the current scene object with the new setting, timeOfDay, weather, and elements from OpenAI
gameState.storyHistory.push(response.narrative);
Adds the narrative text to the story history array to maintain a record of all events
if (response.newItem && !gameState.inventory.includes(response.newItem))
Checks if there's a new item AND it's not already in inventory (prevents duplicates)
gameState.inventory.push(response.newItem);
Adds the new item to the inventory array
narrativeTextP.html(`${response.narrative}<br><br>You found a ${response.newItem}!`);
Updates the narrative text on the page to include both the story and the item found message
speak(`${response.narrative}. You found a ${response.newItem}!`);
Uses text-to-speech to read the narrative and item message aloud
updateUI(response.choices);
Updates the UI with new choice buttons and refreshed health/inventory displays
if (gameState.health <= 0 || response.gameOver)
Checks if the player died (health ≀ 0) or if OpenAI indicated the game should end
gameOverScreen.style('display', 'flex');
Shows the game over screen by setting its CSS display property to 'flex'
else if (response.victory)
Checks if OpenAI indicated the player has won
victoryScreen.style('display', 'flex');
Shows the victory screen by setting its CSS display property to 'flex'

updateUI(choices = [])

updateUI() refreshes all user interface elements based on the current game state. It updates the health bar (using CSS custom properties), location text, inventory list, and choice buttons. The function uses p5.js DOM functions like createElement() and child() to dynamically build HTML elements.

function updateUI(choices = []) {
  let healthPercentage = (gameState.health / MAX_HEALTH) * 100;
  healthBarDiv.style('--health-percentage', `${healthPercentage}%`);
  healthTextSpan.html(`${gameState.health}%`);

  locationTextDiv.html(`Location: ${gameState.location}`);

  inventoryListUl.html('');
  if (gameState.inventory.length === 0) {
    inventoryListUl.html('<li>Empty</li>');
  } else {
    for (let item of gameState.inventory) {
      inventoryListUl.child(createElement('li', item));
    }
  }

  choicesContainer.html('');
  if (!gameState.gameOver && !gameState.victory && choices.length > 0) {
    for (let choice of choices) {
      let button = createButton(choice.text);
      button.mousePressed(() => handleChoice(choice.text));
      choicesContainer.child(button);
    }
  }
}

πŸ”§ Subcomponents:

calculation Health Percentage Calculation let healthPercentage = (gameState.health / MAX_HEALTH) * 100;

Converts the health value (0-100) to a percentage for the CSS variable

for-loop Inventory Display Loop for (let item of gameState.inventory)

Iterates through all items in inventory and creates list elements for each

for-loop Choice Buttons Loop for (let choice of choices)

Creates a button for each choice option and attaches click handlers

conditional Display Choices Only If Game Active if (!gameState.gameOver && !gameState.victory && choices.length > 0)

Prevents showing choice buttons when the game has ended

Line by Line:

let healthPercentage = (gameState.health / MAX_HEALTH) * 100;
Converts health (0-100) to a percentage (0-100%) for the CSS progress bar
healthBarDiv.style('--health-percentage', `${healthPercentage}%`);
Sets a CSS custom property (--health-percentage) that controls the width of the health bar fill
healthTextSpan.html(`${gameState.health}%`);
Updates the health text display to show the current health value
locationTextDiv.html(`Location: ${gameState.location}`);
Updates the location text to show where the player currently is
inventoryListUl.html('');
Clears the inventory list by removing all child elements
if (gameState.inventory.length === 0) { inventoryListUl.html('<li>Empty</li>'); }
If inventory is empty, displays 'Empty' message; otherwise, loops through items
for (let item of gameState.inventory)
Uses for-of loop to iterate through each item in the inventory array
inventoryListUl.child(createElement('li', item));
Creates a new list item element with the item name and adds it to the inventory list
choicesContainer.html('');
Clears all previous choice buttons from the container
if (!gameState.gameOver && !gameState.victory && choices.length > 0)
Only displays choice buttons if the game is still active AND there are choices to show
let button = createButton(choice.text);
Creates a new button element with the choice text
button.mousePressed(() => handleChoice(choice.text));
Attaches a click handler that calls handleChoice() with the choice text when clicked
choicesContainer.child(button);
Adds the button to the choices container so it appears on the page

restartGame()

restartGame() is called when the restart button is clicked. It simply calls startGame() to reset everything and hides the end-game screens. This allows the player to start a completely new adventure.

function restartGame() {
  startGame();
  gameOverScreen.style('display', 'none');
  victoryScreen.style('display', 'none');
}

Line by Line:

startGame();
Calls startGame() to reset the game state and begin a new adventure
gameOverScreen.style('display', 'none');
Hides the game over screen by setting display to 'none'
victoryScreen.style('display', 'none');
Hides the victory screen by setting display to 'none'

speak(text)

speak() uses the Web Speech API to narrate the story aloud. It creates a SpeechSynthesisUtterance object, assigns a voice to it, and calls synth.speak(). The synth.cancel() call ensures only one voice is speaking at a time.

function speak(text) {
  if (synth && voice) {
    let utterance = new SpeechSynthesisUtterance(text);
    utterance.voice = voice;
    utterance.rate = 1;
    synth.cancel();
    synth.speak(utterance);
  }
}

πŸ”§ Subcomponents:

conditional Voice Availability Check if (synth && voice)

Ensures speech synthesis and a voice are available before attempting to speak

Line by Line:

if (synth && voice)
Checks that both the speech synthesis API and a voice are available before proceeding
let utterance = new SpeechSynthesisUtterance(text);
Creates a new utterance object containing the text to be spoken
utterance.voice = voice;
Assigns the selected voice (English male) to the utterance
utterance.rate = 1;
Sets the speech rate to 1 (normal speed; 0.5 would be half speed, 2 would be double)
synth.cancel();
Stops any currently playing speech so the new text can be spoken without overlap
synth.speak(utterance);
Begins speaking the utterance using the browser's text-to-speech engine

drawScene()

drawScene() is the main orchestrator for rendering the procedural environment. It's called every frame from draw(). It layers the scene in order: background (time of day), setting (forest/cave/etc), weather, and elements. This layering creates depth and visual coherence.

function drawScene() {
  drawTimeOfDay();

  switch (gameState.currentScene.setting) {
    case 'forest':
      drawForest();
      break;
    case 'cave':
      drawCave();
      break;
    case 'castle':
      drawCastle();
      break;
    case 'village':
      drawVillage();
      break;
    case 'river':
      drawRiver();
      break;
    default:
      background(0, 50, 100);
      break;
  }

  drawWeather();
  drawElements();
}

πŸ”§ Subcomponents:

assignment Time of Day Background drawTimeOfDay();

Draws the sky gradient based on the current time of day

switch-case Setting Router switch (gameState.currentScene.setting)

Routes to the appropriate drawing function based on the current location

assignment Weather Effects drawWeather();

Draws weather effects like rain, fog, or storms over the scene

assignment Scene Elements drawElements();

Draws interactive elements like trees, chests, monsters, and NPCs

Line by Line:

drawTimeOfDay();
Calls drawTimeOfDay() to render the sky gradient based on dawn/day/dusk/night
switch (gameState.currentScene.setting)
Routes to different drawing functions based on the setting (forest, cave, castle, village, or river)
case 'forest': drawForest(); break;
If the setting is 'forest', calls drawForest() to render trees and foliage
drawWeather();
Draws weather effects (rain, fog, storm, or clear) over the scene
drawElements();
Draws interactive elements like trees, rocks, chests, monsters, NPCs, fire, and water

drawTimeOfDay()

drawTimeOfDay() creates the sky background with a gradient that changes based on time of day. For night, it also draws random stars. The gradient creates a smooth transition from one color to another, making the sky look more realistic than a solid color.

function drawTimeOfDay() {
  let c1, c2;
  switch (gameState.currentScene.timeOfDay) {
    case 'dawn':
      c1 = color(255, 150, 0);
      c2 = color(100, 150, 255);
      break;
    case 'day':
      c1 = color(100, 150, 255);
      c2 = color(200, 220, 255);
      break;
    case 'dusk':
      c1 = color(150, 50, 200);
      c2 = color(255, 100, 0);
      break;
    case 'night':
      c1 = color(20, 20, 70);
      c2 = color(0, 0, 0);
      fill(255);
      noStroke();
      for (let i = 0; i < 100; i++) {
        circle(random(width), random(height * 0.7), random(1, 3));
      }
      break;
    default:
      c1 = color(100, 150, 255);
      c2 = color(200, 220, 255);
      break;
  }
  setGradient(0, 0, width, height, c1, c2);
}

πŸ”§ Subcomponents:

switch-case Time of Day Color Selection switch (gameState.currentScene.timeOfDay)

Selects appropriate colors for the sky based on time of day

for-loop Star Generation for (let i = 0; i < 100; i++)

Draws 100 random stars in the night sky

Line by Line:

let c1, c2;
Declares two color variables for the gradient (top and bottom colors)
switch (gameState.currentScene.timeOfDay)
Routes to different color schemes based on whether it's dawn, day, dusk, or night
case 'dawn': c1 = color(255, 150, 0); c2 = color(100, 150, 255);
Dawn has orange at top (sun rising) and light blue at bottom
case 'day': c1 = color(100, 150, 255); c2 = color(200, 220, 255);
Day has light blue at top and pale blue at bottom for a bright sky
case 'dusk': c1 = color(150, 50, 200); c2 = color(255, 100, 0);
Dusk has purple at top (dark sky) and orange at bottom (setting sun)
case 'night': c1 = color(20, 20, 70); c2 = color(0, 0, 0);
Night has dark blue at top and black at bottom
for (let i = 0; i < 100; i++) { circle(random(width), random(height * 0.7), random(1, 3)); }
Draws 100 stars at random positions in the upper 70% of the canvas with random sizes
setGradient(0, 0, width, height, c1, c2);
Calls setGradient() to blend the two colors from top to bottom across the entire canvas

setGradient(x, y, w, h, c1, c2)

setGradient() creates a smooth color gradient by drawing many thin horizontal lines, each with a slightly different color. It uses lerpColor() to interpolate between two colors and map() to convert row positions to interpolation values. This technique is simple but effective for creating smooth color transitions.

function setGradient(x, y, w, h, c1, c2) {
  noFill();
  for (let i = y; i <= y + h; i++) {
    let inter = map(i, y, y + h, 0, 1);
    let c = lerpColor(c1, c2, inter);
    stroke(c);
    line(x, i, x + w, i);
  }
}

πŸ”§ Subcomponents:

for-loop Horizontal Line Loop for (let i = y; i <= y + h; i++)

Draws horizontal lines from top to bottom, each with a slightly different color

calculation Color Blending let c = lerpColor(c1, c2, inter);

Interpolates between two colors to create a smooth gradient

Line by Line:

noFill();
Disables fill so only the stroked lines are drawn
for (let i = y; i <= y + h; i++)
Loops through each pixel row from y to y+h (the full height of the gradient)
let inter = map(i, y, y + h, 0, 1);
Maps the current row position to a value between 0 and 1 (0 at top, 1 at bottom)
let c = lerpColor(c1, c2, inter);
Interpolates between color 1 and color 2 based on the inter value (0 = c1, 1 = c2)
stroke(c);
Sets the stroke color to the interpolated color
line(x, i, x + w, i);
Draws a horizontal line at row i with the current color, creating one stripe of the gradient

drawWeather()

drawWeather() adds atmospheric effects on top of the scene. Rain is drawn as diagonal lines, fog as a semi-transparent overlay, and storms as a dark overlay with occasional lightning. The random() < 0.02 check gives lightning a 2% chance to appear each frame, creating a flickering effect.

function drawWeather() {
  noFill();
  stroke(255, 255, 255, 150);
  switch (gameState.currentScene.weather) {
    case 'rain':
      stroke(100, 100, 255, 150);
      for (let i = 0; i < 200; i++) {
        let x = random(width);
        let y = random(height);
        line(x, y, x + 5, y + 15);
      }
      break;
    case 'fog':
      fill(255, 255, 255, 50);
      noStroke();
      rect(0, 0, width, height);
      break;
    case 'storm':
      fill(0, 0, 0, 100);
      noStroke();
      rect(0, 0, width, height);
      if (random() < 0.02) {
        stroke(255, 255, 0);
        strokeWeight(3);
        let startX = random(width);
        let startY = 0;
        let endX = random(width);
        let endY = height * 0.7;
        line(startX, startY, startX + random(-50, 50), startY + random(50, 100));
        line(startX + random(-50, 50), startY + random(50, 100), endX, endY);
      }
      break;
    case 'clear':
      break;
  }
}

πŸ”§ Subcomponents:

for-loop Rain Drops for (let i = 0; i < 200; i++)

Draws 200 random diagonal lines to represent falling rain

conditional Random Lightning Flash if (random() < 0.02)

Has a 2% chance per frame to draw a lightning bolt

Line by Line:

switch (gameState.currentScene.weather)
Routes to different weather drawing code based on the weather type
case 'rain': stroke(100, 100, 255, 150);
Sets stroke to blue-tinted color for rain
for (let i = 0; i < 200; i++) { line(x, y, x + 5, y + 15); }
Draws 200 diagonal lines at random positions to create the effect of falling rain
case 'fog': fill(255, 255, 255, 50); rect(0, 0, width, height);
Draws a semi-transparent white rectangle over the entire scene to create a fog effect
case 'storm': fill(0, 0, 0, 100); rect(0, 0, width, height);
Draws a semi-transparent dark overlay to darken the scene during a storm
if (random() < 0.02)
Has a 2% chance each frame to draw lightning (creates a flickering effect)
line(startX, startY, startX + random(-50, 50), startY + random(50, 100));
Draws the first segment of a lightning bolt with random jitter to create a zigzag pattern
case 'clear': break;
For clear weather, do nothing (the sky is already drawn)

drawForest()

drawForest() creates a simple forest scene with 5 trees. Each tree has a brown trunk and green canopy. The trees are spread across the width using map(), and heights are randomized for visual variety. This demonstrates how procedural generation can create different scenes from the same code.

function drawForest() {
  fill(139, 69, 19);
  noStroke();
  for (let i = 0; i < 5; i++) {
    let x = map(i, 0, 4, width * 0.1, width * 0.9);
    let trunkHeight = random(height * 0.4, height * 0.6);
    rect(x, height - trunkHeight, 20, trunkHeight);
    fill(34, 139, 34);
    circle(x + 10, height - trunkHeight - 50, random(80, 120));
  }
}

πŸ”§ Subcomponents:

for-loop Tree Generation for (let i = 0; i < 5; i++)

Draws 5 trees spread across the canvas width

Line by Line:

fill(139, 69, 19);
Sets fill color to brown for tree trunks
for (let i = 0; i < 5; i++)
Loops 5 times to create 5 trees
let x = map(i, 0, 4, width * 0.1, width * 0.9);
Maps tree index (0-4) to x positions spread from 10% to 90% of canvas width
let trunkHeight = random(height * 0.4, height * 0.6);
Generates a random trunk height between 40% and 60% of canvas height for variety
rect(x, height - trunkHeight, 20, trunkHeight);
Draws a brown rectangle (trunk) with width 20, positioned at the bottom of the canvas
fill(34, 139, 34); circle(x + 10, height - trunkHeight - 50, random(80, 120));
Draws a green circle (canopy) above the trunk with random size for natural variation

drawCave()

drawCave() creates a dark cave scene with stalactites hanging from the top and stalagmites rising from the bottom. Note: there's a bug in this functionβ€”height(canvas) should be just height (the canvas height variable). The triangles create a simple but effective cave atmosphere.

function drawCave() {
  fill(50);
  noStroke();
  rect(0, 0, width, height);
  fill(100);
  for (let i = 0; i < 10; i++) {
    let x = map(i, 0, 9, width * 0.1, width * 0.9);
    let height = random(50, 150);
    triangle(x, 0, x - 20, height, x + 20, height);
    triangle(x, height(canvas), x - 20, height(canvas) - height, x + 20, height(canvas) - height);
  }
}

πŸ”§ Subcomponents:

for-loop Stalactite/Stalagmite Generation for (let i = 0; i < 10; i++)

Draws 10 pairs of stalactites and stalagmites

Line by Line:

fill(50); rect(0, 0, width, height);
Fills the entire canvas with dark grey to represent the cave interior
fill(100);
Changes fill color to lighter grey for the stalactites and stalagmites
for (let i = 0; i < 10; i++)
Loops 10 times to create 10 pairs of cave formations
let x = map(i, 0, 9, width * 0.1, width * 0.9);
Maps formation index to x positions spread across the canvas width
let height = random(50, 150);
Generates a random height for the formations (50-150 pixels)
triangle(x, 0, x - 20, height, x + 20, height);
Draws a stalactite (hanging from top) as an inverted triangle
triangle(x, height(canvas), x - 20, height(canvas) - height, x + 20, height(canvas) - height);
Draws a stalagmite (rising from bottom) as an upright triangle

drawCastle()

drawCastle() creates a castle scene with a main wall, crenellations (the notched top), and two towers with peaked roofs. The crenellation loop uses a step increment (i += 40) instead of i++ to space them evenly. This demonstrates how different loop patterns can create different visual effects.

function drawCastle() {
  fill(150);
  noStroke();
  rect(0, height * 0.4, width, height * 0.6);
  for (let i = 0; i < width; i += 40) {
    rect(i, height * 0.3, 20, height * 0.1);
  }
  fill(120);
  for (let i = 0; i < 2; i++) {
    let x = map(i, 0, 1, width * 0.2, width * 0.8);
    rect(x, height * 0.2, 80, height * 0.4);
    fill(100);
    triangle(x, height * 0.2, x + 80, height * 0.2, x + 40, height * 0.1);
  }
}

πŸ”§ Subcomponents:

for-loop Castle Crenellations for (let i = 0; i < width; i += 40)

Draws the crenellated (notched) top of the castle wall

for-loop Tower Generation for (let i = 0; i < 2; i++)

Draws 2 towers on either side of the castle

Line by Line:

fill(150); rect(0, height * 0.4, width, height * 0.6);
Draws the main castle wall as a grey rectangle from 40% down to the bottom
for (let i = 0; i < width; i += 40)
Loops across the width in 40-pixel increments to create evenly-spaced crenellations
rect(i, height * 0.3, 20, height * 0.1);
Draws a small rectangle (crenellation) above the main wall
fill(120); for (let i = 0; i < 2; i++)
Changes color to darker grey and loops twice to create 2 towers
let x = map(i, 0, 1, width * 0.2, width * 0.8);
Maps tower index (0-1) to x positions: first tower at 20% width, second at 80%
rect(x, height * 0.2, 80, height * 0.4);
Draws a tower as a tall rectangle
triangle(x, height * 0.2, x + 80, height * 0.2, x + 40, height * 0.1);
Draws a triangular roof on top of the tower

drawVillage()

drawVillage() creates a village scene with 3 houses. Each house has a tan body and brown roof. The houses are positioned at different x positions and have random heights. This is similar to drawForest() but creates a different aesthetic.

function drawVillage() {
  fill(200, 150, 100);
  noStroke();
  for (let i = 0; i < 3; i++) {
    let x = map(i, 0, 2, width * 0.15, width * 0.75);
    let houseWidth = 100;
    let houseHeight = random(80, 120);
    rect(x, height - houseHeight, houseWidth, houseHeight);
    fill(150, 100, 50);
    triangle(x, height - houseHeight, x + houseWidth, height - houseHeight, x + houseWidth / 2, height - houseHeight - 50);
  }
}

πŸ”§ Subcomponents:

for-loop House Generation for (let i = 0; i < 3; i++)

Draws 3 houses spread across the village

Line by Line:

fill(200, 150, 100);
Sets fill to warm tan color for house walls
for (let i = 0; i < 3; i++)
Loops 3 times to create 3 houses
let x = map(i, 0, 2, width * 0.15, width * 0.75);
Maps house index (0-2) to x positions spread from 15% to 75% of canvas width
let houseHeight = random(80, 120);
Generates a random house height between 80 and 120 pixels for variety
rect(x, height - houseHeight, houseWidth, houseHeight);
Draws a tan rectangle for the house body, positioned at the bottom of the canvas
fill(150, 100, 50); triangle(...);
Changes fill to darker brown and draws a triangular roof on top of the house

drawRiver()

drawRiver() creates an animated river with a wavy surface. The key technique is using sin(x * 0.05 + frameCount * 0.02) to create a sine wave that animates over time. frameCount increases every frame, so adding it to the sine argument makes the wave move. The beginShape()/vertex()/endShape() pattern creates a custom polygon shape.

function drawRiver() {
  fill(50, 100, 200);
  noStroke();
  beginShape();
  vertex(0, height * 0.7);
  for (let x = 0; x <= width; x += 20) {
    let y = height * 0.7 + sin(x * 0.05 + frameCount * 0.02) * 20;
    vertex(x, y);
  }
  vertex(width, height);
  vertex(0, height);
  endShape(CLOSE);
  fill(100);
  for (let i = 0; i < 5; i++) {
    circle(random(width), random(height * 0.8, height * 0.95), random(20, 50));
  }
}

πŸ”§ Subcomponents:

for-loop Wavy River Shape for (let x = 0; x <= width; x += 20)

Creates a wavy river surface using sine wave animation

for-loop River Rocks for (let i = 0; i < 5; i++)

Draws 5 rocks in the river

Line by Line:

fill(50, 100, 200);
Sets fill to blue color for the river
beginShape();
Starts defining a custom shape using vertices
vertex(0, height * 0.7);
Adds the first vertex at the top-left of the river
for (let x = 0; x <= width; x += 20)
Loops across the canvas width in 20-pixel increments
let y = height * 0.7 + sin(x * 0.05 + frameCount * 0.02) * 20;
Calculates a wavy y position using sine wave: sin() creates the wave, frameCount makes it animate, the 0.02 controls animation speed
vertex(x, y);
Adds a vertex at the calculated wavy position
vertex(width, height); vertex(0, height);
Adds vertices at the bottom corners to close the shape
endShape(CLOSE);
Closes the shape by connecting the last vertex back to the first
fill(100); for (let i = 0; i < 5; i++) { circle(...); }
Draws 5 grey circles (rocks) at random positions in the river

drawElements()

drawElements() draws interactive scene elements (trees, rocks, chests, monsters, NPCs, fire, water) at random positions. It loops through the elements array from the current scene and uses a switch statement to draw each element type differently. Random positioning within a foreground area creates visual variety while keeping elements visible.

function drawElements() {
  for (let element of gameState.currentScene.elements) {
    noStroke();
    let x = random(width * 0.1, width * 0.9);
    let y = random(height * 0.6, height * 0.9);

    switch (element) {
      case 'tree':
        fill(139, 69, 19);
        rect(x, y, 20, 80);
        fill(34, 139, 34);
        circle(x + 10, y, 60);
        break;
      case 'rock':
        fill(100);
        circle(x, y, 40);
        break;
      case 'chest':
        fill(160, 120, 80);
        rect(x, y, 50, 40);
        fill(200, 160, 120);
        rect(x + 10, y + 10, 30, 20);
        break;
      case 'monster':
        fill(200, 50, 50);
        circle(x, y, 50);
        fill(0);
        circle(x - 10, y - 5, 5);
        circle(x + 10, y - 5, 5);
        arc(x, y + 10, 30, 20, 0, PI);
        break;
      case 'npc':
        fill(255, 200, 150);
        circle(x, y, 40);
        fill(0);
        circle(x - 5, y - 5, 3);
        circle(x + 5, y - 5, 3);
        line(x - 5, y + 10, x + 5, y + 10);
        break;
      case 'fire':
        fill(255, 150, 0);
        triangle(x, y, x - 15, y - 30, x + 15, y - 30);
        fill(255, 0, 0);
        triangle(x, y, x - 10, y - 20, x + 10, y - 20);
        break;
      case 'water':
        fill(50, 100, 200);
        rect(x, y, 80, 20);
        break;
    }
  }
}

πŸ”§ Subcomponents:

for-loop Element Iteration for (let element of gameState.currentScene.elements)

Loops through all elements in the current scene

switch-case Element Type Router switch (element)

Routes to different drawing code based on element type

Line by Line:

for (let element of gameState.currentScene.elements)
Uses for-of loop to iterate through each element in the current scene's elements array
let x = random(width * 0.1, width * 0.9);
Generates a random x position between 10% and 90% of canvas width
let y = random(height * 0.6, height * 0.9);
Generates a random y position in the lower 30% of the canvas (where foreground objects should be)
switch (element)
Routes to different drawing code based on the element type (tree, rock, chest, etc.)
case 'tree': fill(139, 69, 19); rect(x, y, 20, 80); fill(34, 139, 34); circle(x + 10, y, 60);
Draws a tree with a brown trunk and green canopy at position (x, y)
case 'monster': fill(200, 50, 50); circle(x, y, 50); ... circle(x - 10, y - 5, 5); ... arc(...);
Draws a monster as a red circle with two black eyes and a mouth arc
case 'chest': fill(160, 120, 80); rect(x, y, 50, 40); fill(200, 160, 120); rect(x + 10, y + 10, 30, 20);
Draws a treasure chest as a dark rectangle with a lighter lid on top
case 'npc': fill(255, 200, 150); circle(x, y, 40); ... circle(x - 5, y - 5, 3); ... line(...);
Draws an NPC as a tan circle with two eyes and a mouth
case 'fire': fill(255, 150, 0); triangle(...); fill(255, 0, 0); triangle(...);
Draws fire as two overlapping triangles: orange outer flame and red inner flame
case 'water': fill(50, 100, 200); rect(x, y, 80, 20);
Draws water as a blue rectangle

getApiKey()

getApiKey() decodes an obfuscated API key using base64 decoding and XOR encryption. This is a simple obfuscation techniqueβ€”it's not cryptographically secure but prevents casual exposure of the key in the source code. The XOR cipher (^) is a bitwise operation that flips bits based on the key value.

function getApiKey(){return atob(encoded).split('').map(c=>String.fromCharCode(c.charCodeAt(0)^key)).join('');}

πŸ”§ Subcomponents:

calculation Base64 Decoding atob(encoded)

Decodes the base64-encoded string

calculation XOR Decryption .map(c=>String.fromCharCode(c.charCodeAt(0)^key))

XORs each character with the key to decrypt the API key

Line by Line:

atob(encoded)
Decodes the base64-encoded string into a regular string
.split('')
Splits the string into an array of individual characters
.map(c=>String.fromCharCode(c.charCodeAt(0)^key))
Maps each character: c.charCodeAt(0) gets its ASCII code, ^ key performs XOR decryption, String.fromCharCode() converts back to a character
.join('')
Joins the decrypted characters back into a single string (the API key)

πŸ“¦ Key Variables

gameState object

Stores all game data including health, inventory, location, story history, and current scene properties. This is the central state object that OpenAI reads and modifies.

let gameState = { health: 100, inventory: [], location: 'Unknown', storyHistory: [], currentScene: {...} };
healthBarDiv object (p5.Renderer)

Cached reference to the HTML health bar element for efficient DOM updates without re-querying

healthBarDiv = select('#health-bar');
healthTextSpan object (p5.Renderer)

Cached reference to the health percentage text element

healthTextSpan = select('#health-text');
locationTextDiv object (p5.Renderer)

Cached reference to the location display element

locationTextDiv = select('#location-text');
narrativeTextP object (p5.Renderer)

Cached reference to the narrative text element where story descriptions are displayed

narrativeTextP = select('#narrative-text');
choicesContainer object (p5.Renderer)

Cached reference to the container where choice buttons are dynamically added

choicesContainer = select('#choices-container');
inventoryListUl object (p5.Renderer)

Cached reference to the unordered list element that displays inventory items

inventoryListUl = select('#inventory-list');
loadingOverlay object (p5.Renderer)

Cached reference to the loading spinner overlay shown during API requests

loadingOverlay = select('#loading-overlay');
gameOverScreen object (p5.Renderer)

Cached reference to the game over screen element shown when the player dies

gameOverScreen = select('#game-over-screen');
victoryScreen object (p5.Renderer)

Cached reference to the victory screen element shown when the player wins

victoryScreen = select('#victory-screen');
restartButton object (p5.Renderer)

Cached reference to the restart button that resets the game

restartButton = select('#restart-button');
synth object (SpeechSynthesis)

Reference to the browser's Web Speech API for text-to-speech narration

synth = window.speechSynthesis;
voice object (SpeechSynthesisVoice)

The selected voice used for narration (preferably English US male)

voice = synth.getVoices().find(v => v.lang === 'en-US' && v.name.includes('Male'));
encoded string

Base64-encoded and XOR-encrypted OpenAI API key

const encoded='KTF3Kig1MHcIaDhqCms+OA0AYhEADSo9NDEbLDxjKwMTPW0fdwgLMQ8wahcvFy4UIDAxYiI4CWgwdy0fLGgsDgwcLCkDAgsuNh40OxwcN24LbA5pGDY4MRwQKR0+AigiLDATP2piFwwqLzk3agorOBM2ExkvLTMeDmg8OwoTGCMpLhQXOWMuNBIPDRc2bRlpFA0THT0RIh8NETsRKyADAykUKRs=';
key number

XOR cipher key used to decrypt the API key (0x5A is hexadecimal for 90)

const key=0x5A;
MAX_HEALTH number

Maximum health value (100), used to cap health and calculate health percentage

const MAX_HEALTH = 100;
OPENAI_API_URL string

The endpoint URL for OpenAI's chat completions API

const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
OPENAI_MODEL string

The AI model to use (gpt-3.5-turbo for cost-effectiveness, or gpt-4 for better quality)

const OPENAI_MODEL = 'gpt-3.5-turbo';
OPENAI_TEMP number

Temperature setting for OpenAI (0.7 balances creativity and consistency; 0=deterministic, 1=very random)

const OPENAI_TEMP = 0.7;
OPENAI_MAX_TOKENS number

Maximum tokens (words) in OpenAI's response (500 tokens β‰ˆ 375 words)

const OPENAI_MAX_TOKENS = 500;

πŸ§ͺ Try This!

Experiment with the code by making these changes:

  1. Change OPENAI_TEMP from 0.7 to 0.3 and play a game. The story will be more consistent and predictable. Then try 1.0 for wild, unpredictable narratives. Observe how temperature affects creativity.
  2. Modify drawForest() to add more trees: change 'for (let i = 0; i < 5; i++)' to 'for (let i = 0; i < 10; i++)'. You'll see a denser forest. Try different numbers to find the right balance.
  3. In drawWeather(), change the rain case to use 'stroke(255, 0, 0, 150)' for red rain instead of blue. See how color changes affect the mood of the scene.
  4. Add a new setting to drawScene() switch statement: 'case "desert": drawDesert(); break;' and create a drawDesert() function that draws sand dunes and cacti. This teaches you how to extend the procedural generation.
  5. Modify the updateGame() function to add a 'score' variable that increases when the player finds items. Add 'gameState.score = (gameState.score || 0) + 10;' after finding an item and display it in updateUI().
  6. Change the speech rate in speak() from 'utterance.rate = 1;' to 'utterance.rate = 0.5;' to make the narration slower and more dramatic.
  7. In drawElements(), add a new case for 'treasure': draw a golden rectangle with circles on it to represent a treasure pile. Then ask OpenAI to include 'treasure' in the elements array.
  8. Modify the health bar color in CSS from red (#e74c3c) to green or blue by changing the background-color in the #health-bar::after selector. See how visual feedback changes player perception.
Open in Editor & Experiment β†’

πŸ”§ Potential Improvements

Here are some ways this code could be enhanced:

BUG drawCave() function

Line uses 'height(canvas)' which is invalid syntax. The height variable is already the canvas height.

πŸ’‘ Change 'triangle(x, height(canvas), ...)' to 'triangle(x, height, ...)' to fix the stalagmite drawing.

PERFORMANCE drawWeather() function, rain case

Draws 200 random lines every frame, which is computationally expensive. With 60 FPS, that's 12,000 line draws per second.

πŸ’‘ Use a createGraphics() buffer to draw rain once and reuse it, or reduce the rain count to 50-100 lines.

PERFORMANCE drawTimeOfDay() function, night case

Generates 100 random stars every frame, causing the stars to flicker. Stars should be static.

πŸ’‘ Generate stars once in setup() and store them in an array, then draw them in drawTimeOfDay() without regenerating.

STYLE callOpenAI() function

Very long function with multiple responsibilities (API call, error handling, parsing, game update). Hard to test and debug.

πŸ’‘ Split into smaller functions: makeAPIRequest(), parseResponse(), handleAPIError(). This follows the Single Responsibility Principle.

FEATURE updateGame() function

No visual feedback when player takes damage. Health just decreases silently.

πŸ’‘ Add a flash effect to the canvas or screen shake when health decreases. Use push/pop and translate to shake the canvas briefly.

BUG handleChoice() function

No validation that the choice text exists or is valid before sending to OpenAI. Could cause issues if choice.text is undefined.

πŸ’‘ Add a check: 'if (!choiceText || choiceText.trim() === "") return;' before processing the choice.

PERFORMANCE drawElements() function

Generates new random x,y positions for every element every frame, causing elements to move around. Should be static or stored.

πŸ’‘ Store element positions in gameState.currentScene.elements as objects with x,y properties, or generate them once per scene change.

FEATURE Game state management

No save/load functionality. Player progress is lost on page refresh.

πŸ’‘ Use localStorage to save gameState: 'localStorage.setItem("gameState", JSON.stringify(gameState))' and load it in setup().

STYLE getApiKey() function

API key is obfuscated but still visible in source code. Not secure for production.

πŸ’‘ Store the API key on a backend server and make requests to your own API instead of exposing the key in client-side code.

Preview

AI Dungeon Master - Procedural Adventure - xelsed.ai - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of AI Dungeon Master - Procedural Adventure - xelsed.ai - Code flow showing setup, draw, windowresized, startgame, handlechoice, callopenai, updategame, updateui, restartgame, speak, drawscene, drawtimeofday, setgradient, drawweather, drawforest, drawcave, drawcastle, drawvillage, drawriver, drawelements, getapikey
Code Flow Diagram