Civilization

This sketch creates a Dungeons & Dragons-themed social simulation app where players can log in, manage resources, join groups, accept bounties, and communicate with party members. The interface uses a cyberpunk aesthetic with DOM elements to build a complete multi-screen application including user authentication, a marketplace, group management, and an admin control panel.

🧪 Try This!

Experiment with the code by making these changes:

  1. Change the starting gold for new players — New accounts are created with 500 gold by default. Change this number to 1000 or 250 to adjust early-game economics.
  2. Make the feed refresh faster when posts arrive — Change how often random group posts appear by adjusting the interval. Lower numbers make the feed more active.
  3. Add a new message template to random group posts — Expand the postMessages array with a new message template. This will make random group posts more varied.
  4. Change the cover screen title — Customize the welcome screen by changing the title text from 'Civilization' to anything else.
Prefer the full editor? Open it there →

📖 About This Sketch

This sketch demonstrates how to build a full-featured web application using p5.js DOM methods rather than canvas drawing. It implements user registration and login, persistent data storage with localStorage, multiple interactive screens, and role-based features where an admin user has special powers. The cyberpunk-themed interface showcases state management, conditional rendering, and event handling—skills that transfer directly to building real interactive web apps.

The code is organized into setup() for initializing all DOM elements, displayContent() for managing screen transitions, and specialized handler functions for login, signup, bounties, groups, and user management. By reading this code you'll learn how to structure a multi-screen application, manage global state, persist data across browser sessions, and build complex interactions where UI elements change based on who is logged in and what they're doing.

⚙️ How It Works

  1. When the sketch first loads, setup() creates all DOM elements (buttons, inputs, containers) for every screen but hides most of them. It loads any previously saved user accounts from localStorage and displays the cover screen with an 'Enter Civilization' button.
  2. Clicking buttons transitions between screens by changing the currentScreen variable and calling displayContent(), which shows/hides containers and regenerates content based on the active screen. The navbar appears after login, letting players navigate to Feed, Marketplace, Groups, Bounties, Party Comms, and Members screens.
  3. When a player signs up, their username, password, character class/race/info, and starting gold (500 gp) are stored in the registeredUsers array and saved to localStorage so they persist after page refresh.
  4. The Admin account (username 'Admin', password 'Password') has access to an AI Control Terminal that can generate new bounties and marketplace items, rename users, edit group names, delete accounts, and manage player gold—all changes post to the party feed for other players to see.
  5. Party Comms lets players type messages that appear in the chat and also post to the main Feed. Random background posts from group members appear automatically every 20–45 seconds, simulating an active world. Accepting a bounty removes it from the list, adds its reward to the player's gold, and posts to the feed.
  6. All player actions (joining groups, accepting bounties, posting messages, receiving gold changes) update the HUD at the top showing current username and gold balance, and trigger feed updates that all players see in real-time.

🎓 Concepts You'll Learn

DOM manipulation with p5.jsMulti-screen state managementUser authentication and registrationlocalStorage persistenceConditional rendering based on user roleEvent-driven programmingArray methods and object filteringTimestamp generation and formatting

📝 Code Breakdown

setup()

setup() runs once when the page loads. In p5.js apps that use DOM elements instead of canvas, setup() becomes a factory function that creates and configures all interactive elements. Notice how many element creation lines are the same pattern: create → assign ID → position/size → assign class. This is the standard workflow for building interactive web UIs with p5.js.

function setup() {
  createCanvas(windowWidth, windowHeight);
  noCanvas(); // Using DOM elements for UI

  loadUsers(); // Load users from localStorage at startup

  // Create Cover Screen Elements (initially visible)
  coverContainer = createDiv('');
  coverContainer.id('cover-container');
  coverContainer.position(0, 0); // Full screen
  coverContainer.size(width, height);
  coverContainer.class('cover-screen'); // Apply cover screen styling

  coverTitle = createElement('h1', 'Civilization');
  coverTitle.parent(coverContainer);
  coverTitle.class('cover-title');

  coverStartButton = createButton('Enter Civilization');
  coverStartButton.parent(coverContainer);
  coverStartButton.class('cover-start-button');
  coverStartButton.mousePressed(() => {
    currentScreen = "login";
    displayContent();
  });

  // ... [truncated: creates 50+ more DOM elements for login, signup, marketplace, etc.] ...

  displayContent();
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

initialization Canvas and DOM Initialization createCanvas(windowWidth, windowHeight); noCanvas();

Creates a p5.js canvas matching the window size, then disables it since this app uses DOM elements (buttons, inputs, divs) instead of canvas drawing

function-call Load Persistent User Data loadUsers();

Retrieves saved user accounts from browser localStorage so players' accounts persist across page refreshes

dom-creation Cover Screen Creation coverContainer = createDiv(''); coverContainer.id('cover-container'); coverContainer.position(0, 0); coverContainer.size(width, height);

Builds the initial welcome screen that greets all visitors with a title and start button before login

createCanvas(windowWidth, windowHeight);
Creates a p5.js canvas that fills the entire window—needed for p5.js to initialize, even though we won't draw to it
noCanvas();
Disables the default canvas since this app uses DOM elements (HTML inputs, buttons, divs) for the UI, not p5.js drawing commands
loadUsers();
Calls loadUsers() to retrieve any previously saved player accounts from localStorage—if this is the player's first visit, this does nothing and the default Admin account remains
coverContainer = createDiv('');
Creates an empty div element using p5.js's createDiv() function; this will be the container for the cover screen
coverContainer.id('cover-container');
Assigns a unique CSS ID to this div so styling rules in style.css can target it
coverContainer.position(0, 0);
Positions the cover screen at the top-left corner of the window (pixel coordinates 0, 0)
coverContainer.size(width, height);
Makes the cover screen as large as the entire window so it fills the viewport completely
coverStartButton.mousePressed(() => { currentScreen = "login"; displayContent(); });
When the player clicks the start button, change currentScreen to 'login' and call displayContent() to refresh the UI and show the login form

displayContent()

displayContent() is the heart of this app's UI engine. It acts as a state renderer: whenever currentScreen changes, calling displayContent() regenerates the entire visible interface to match the new screen. This pattern (storing a currentScreen variable and having a render function that responds to it) is fundamental to how modern web apps work. Every time a player clicks a button, we change currentScreen and call displayContent() to refresh.

🔬 This forEach loop creates a div for every feed item. What happens if you add feedDiv.style('border', '3px solid red'); before feedDiv.parent()? The border will appear on every feed item. Can you change the color or add a background-color too?

      feedItems.forEach(feedItem => {
        const feedDiv = createDiv('');
        feedDiv.class('feed-item');
        feedDiv.parent(contentDiv);
function displayContent() {
  // Hide all main containers first
  coverContainer.hide();
  loginContainer.hide();
  signupContainer.hide();
  hudDiv.hide();
  navBar.hide();
  contentDiv.hide();
  aiConsoleDiv.hide();
  // ... [more .hide() calls] ...

  // Update HUD content if user is logged in
  if (currentUser) {
    updateHud();
    hudDiv.show();
  }

  // Define top position for navbar and content based on HUD visibility
  const hudHeight = currentUser ? 40 : 0;
  const navbarTop = 10 + hudHeight;
  const contentTop = navbarTop + 65;

  navBar.position(10, navbarTop);
  contentDiv.position(10, contentTop);
  aiConsoleDiv.position(width * 0.6 + 10, contentTop);

  // Position AI console next to feed (only if Home screen and Admin)
  if (currentScreen === "home" && currentUser === correctUsername3) {
    contentDiv.size(width * 0.6 - 30, height - contentTop - 10);
    aiConsoleDiv.size(width * 0.4 - 20, height - contentTop - 10);
    aiConsoleDiv.show();
    aiRenameUserContainer.show();
    aiEditGroupContainer.show();
    aiDeleteUserContainer.show();
    aiGoldContainer.show();
  } else {
    contentDiv.size(width - 20, height - contentTop - 10);
  }

  switch (currentScreen) {
    case "cover":
      coverContainer.show();
      break;

    case "login":
      loginContainer.show();
      break;

    case "home":
      navBar.show();
      contentDiv.show();
      if (currentUser === correctUsername3) {
        aiConsoleDiv.show();
      }
      contentDiv.html('<div class="section-title">Party Feed</div>');
      feedItems.forEach(feedItem => {
        const feedDiv = createDiv('');
        feedDiv.class('feed-item');
        feedDiv.parent(contentDiv);

        let icon = '';
        let typeColor = '';
        switch (feedItem.type) {
          case "bounty_accepted":
            icon = '📜';
            typeColor = '#f1c40f';
            break;
          case "item_found":
            icon = '✨';
            typeColor = '#a2d2ff';
            break;
          // ... more cases ...
        }

        feedDiv.html(`
          <div class="feed-header">
            <span class="feed-icon" style="color: ${typeColor};">${icon}</span>
            <span class="feed-timestamp">${feedItem.timestamp}</span>
          </div>
          <div class="feed-content">${feedItem.content}</div>
        `);
      });
      break;

    case "marketplace":
      navBar.show();
      contentDiv.show();
      contentDiv.html('<div class="section-title">Marketplace</div>');
      marketplaceItems.forEach(item => {
        const itemDiv = createDiv('');
        itemDiv.class('item');
        itemDiv.parent(contentDiv);
        itemDiv.html(`
          <div class="item-details">
            <div class="item-name">${item.name}</div>
            <div class="item-description">${item.description}</div>
          </div>
          <div class="item-price">${item.price}</div>
        `);
        itemDiv.mousePressed(() => showMarketplaceDetail(item));
      });
      break;

    // ... [more cases for groups, bounties, comms, members] ...
  }
}
Line-by-line explanation (11 lines)

🔧 Subcomponents:

loop Hide All Containers coverContainer.hide(); loginContainer.hide(); signupContainer.hide(); // ... many more .hide() calls

Ensures only one screen is visible at a time by hiding every container before deciding which one to show

conditional Show HUD if Logged In if (currentUser) { updateHud(); hudDiv.show(); }

If a player is logged in, update and display the HUD (showing username and gold); if not logged in, hide it

calculation Dynamic Layout Positioning const hudHeight = currentUser ? 40 : 0; const navbarTop = 10 + hudHeight; const contentTop = navbarTop + 65;

Calculates positions for navbar and content based on whether the HUD is visible, ensuring elements stack properly

conditional Admin Split-Screen Layout if (currentScreen === "home" && currentUser === correctUsername3) { contentDiv.size(width * 0.6 - 30, height - contentTop - 10); aiConsoleDiv.size(width * 0.4 - 20, height - contentTop - 10); aiConsoleDiv.show(); }

When the Admin user is on the home screen, split the display 60/40 to show the feed on the left and AI control panel on the right

for-loop Render Feed Items feedItems.forEach(feedItem => { const feedDiv = createDiv(''); feedDiv.class('feed-item'); feedDiv.parent(contentDiv); // ... icon/color assignment ... feedDiv.html(...); });

Loops through each feed item and creates a styled div for it with the appropriate icon color based on the item type

switch-case Screen-Specific Content Generation switch (currentScreen) { case "cover": coverContainer.show(); break; case "login": loginContainer.show(); break; // ... many more cases

A big switch statement that shows/hides and regenerates content for each screen depending on which one is active

coverContainer.hide();
Hides the cover screen by setting its CSS display property to 'none'. This prevents it from taking up space or being clickable.
if (currentUser) { updateHud(); hudDiv.show(); }
If a user is logged in (currentUser is not null), update the HUD display to show their username and gold, then show the HUD container. If no one is logged in, the HUD stays hidden.
const hudHeight = currentUser ? 40 : 0;
A ternary operator: if currentUser exists, set hudHeight to 40 pixels; otherwise, set it to 0. This is used to position other elements below the HUD.
const navbarTop = 10 + hudHeight;
Calculate where the navbar should start (10 pixels from the top plus the HUD height). If no HUD is visible, the navbar starts at 10. If HUD is visible, it starts at 50.
if (currentScreen === "home" && currentUser === correctUsername3) {
Check if the current screen is 'home' AND the logged-in user is the Admin. Only if both are true do we show the AI console and split the screen.
contentDiv.size(width * 0.6 - 30, height - contentTop - 10);
Resize the content div to be 60% of the screen width (minus 30 pixels for padding) when the admin is on the home screen, making room for the AI console beside it.
feedItems.forEach(feedItem => {
Loop through every item in the feedItems array. For each one, we'll create a visual representation.
const feedDiv = createDiv('');
Create a new empty div element that will hold this feed item's content.
feedDiv.parent(contentDiv);
Attach this newly created feedDiv as a child of contentDiv, making it appear inside the content area.
switch (feedItem.type) { case "bounty_accepted": icon = '📜'; ...
Assign a different icon emoji and color based on the feed item's type. Bounties get a scroll emoji, items found get sparkles, etc.
switch (currentScreen) { case "cover": coverContainer.show(); break;
The big screen router: check what screen should be shown (currentScreen) and then show the appropriate container and render its content. Each case handles one screen.

handleLogin()

handleLogin() demonstrates a classic authentication pattern: retrieve credentials from a form, search a data store for a match, and conditionally show/hide UI elements based on the result. The key learning is using .find() to search arrays, checking user roles (correctUsername3) to gate features, and using setTimeout() to create loading experiences. In a real app, you'd send credentials to a secure backend server instead of checking them locally.

🔬 The .find() method searches for a user with matching username AND password. What happens if you change && (AND) to || (OR)? Try removing the password check entirely and using just u.username === username. Can you log in with any password?

  const user = registeredUsers.find(u => u.username === username && u.password === password);

  if (user) {
    loginMessage.html('Access Granted. Redirecting...');
    loginMessage.style('color', '#00ffaa');
function handleLogin() {
  const username = usernameInput.value();
  const password = passwordInput.value();

  console.log(`Attempting login for: Username='${username}', Password='${password}'`);
  console.log(`Admin credentials: Username='${correctUsername3}', Password='${correctPassword3}'`);

  const user = registeredUsers.find(u => u.username === username && u.password === password);

  if (user) {
    loginMessage.html('Access Granted. Redirecting...');
    loginMessage.style('color', '#00ffaa');
    setTimeout(() => {
      currentUser = username;
      currentUserGold = user.gold;
      currentScreen = "home";
      loginContainer.hide();
      hudDiv.show();
      navBar.show();
      contentDiv.show();

      if (currentUser === correctUsername3) {
        aiConsoleDiv.show();
        aiRenameUserContainer.show();
        aiEditGroupContainer.show();
        aiDeleteUserContainer.show();
        aiGoldContainer.show();
      } else {
        aiConsoleDiv.hide();
        aiRenameUserContainer.hide();
        aiEditGroupContainer.hide();
        aiDeleteUserContainer.hide();
        aiGoldContainer.hide();
      }

      if (!groupPostInterval) {
        groupPostInterval = setInterval(randomGroupPost, 20000);
      }
      if (!bountyPostInterval) {
        bountyPostInterval = setInterval(randomBountyPost, 45000);
      }

      displayContent();
    }, 1000);
  } else {
    loginMessage.html('Login Failed. Invalid credentials.');
    loginMessage.style('color', '#ff3333');
  }
}
Line-by-line explanation (10 lines)

🔧 Subcomponents:

initialization Retrieve Form Inputs const username = usernameInput.value(); const password = passwordInput.value();

Extract the text the player typed into the username and password input fields

calculation Search Registered Users const user = registeredUsers.find(u => u.username === username && u.password === password);

Use the .find() array method to search registeredUsers for someone matching both the entered username AND password. Returns the user object if found, or undefined if not.

conditional Validate Credentials if (user) { ... } else { ... }

If a matching user was found, proceed with login. Otherwise, show an error message.

conditional Show Admin Panel if Admin Logs In if (currentUser === correctUsername3) { aiConsoleDiv.show(); aiRenameUserContainer.show(); // ... }

If the user who just logged in is the Admin account, show the AI control console and its sub-panels. For regular players, hide it.

conditional Start Random Post Intervals if (!groupPostInterval) { groupPostInterval = setInterval(randomGroupPost, 20000); } if (!bountyPostInterval) { bountyPostInterval = setInterval(randomBountyPost, 45000); }

Begin the background timers that periodically generate random group posts and bounties while the player is logged in. Check !groupPostInterval to avoid starting multiple intervals.

const username = usernameInput.value();
Get the text the player typed into the username input field and store it in the username variable
const password = passwordInput.value();
Get the text the player typed into the password input field and store it in the password variable
const user = registeredUsers.find(u => u.username === username && u.password === password);
Search the registeredUsers array for a user object where the username AND password both match what was entered. The .find() method returns the first matching object or undefined.
if (user) {
If a user was found (meaning the username and password match), enter this block. If user is undefined, skip to the else block.
currentUser = username;
Store the successfully logged-in username in the global currentUser variable so we know who is playing
currentUserGold = user.gold;
Copy the user's gold amount to the global currentUserGold variable so the HUD can display it
currentScreen = "home";
Set the next screen to show to 'home', which is the main feed screen
setTimeout(() => { ... }, 1000);
Wrap the UI updates in a setTimeout with 1000 milliseconds (1 second) to simulate a loading delay, making the login feel more realistic
if (currentUser === correctUsername3) {
Check if the user who just logged in has the username 'Admin'. If yes, show the admin-only UI elements.
if (!groupPostInterval) { groupPostInterval = setInterval(randomGroupPost, 20000); }
Only start the group post interval if it's not already running (checking !groupPostInterval). This prevents duplicate timers from being created if the player logs in/out multiple times.

handleSignup()

handleSignup() is a multi-step validation workflow: collect inputs, check for errors at each step (empty fields, password mismatch, duplicate username), and only on success create the user object and persist it. Each validation uses an early return statement to avoid cascading logic. This pattern scales well—you could add email validation, password strength checks, or age verification without restructuring the function.

function handleSignup() {
  const username = signupUsernameInput.value();
  const password = signupPasswordInput.value();
  const confirmPassword = signupConfirmPasswordInput.value();
  const characterClass = signupCharacterClassInput.value();
  const characterRace = signupCharacterRaceInput.value();
  const characterInfo = signupCharacterInfoInput.value();

  signupMessage.html('');

  if (!username || !password || !confirmPassword || !characterClass || !characterRace || !characterInfo) {
    signupMessage.html('All fields are required.');
    signupMessage.style('color', '#ff3333');
    return;
  }

  if (password !== confirmPassword) {
    signupMessage.html('Passwords do not match.');
    signupMessage.style('color', '#ff3333');
    return;
  }

  if (registeredUsers.some(u => u.username === username)) {
    signupMessage.html('Username already taken.');
    signupMessage.style('color', '#ff3333');
    return;
  }

  registeredUsers.push({
    username,
    password,
    gold: 500,
    characterClass,
    characterRace,
    characterInfo
  });
  saveUsers();
  signupMessage.html('Account created successfully! Redirecting to login...');
  signupMessage.style('color', '#00ffaa');

  signupUsernameInput.value('');
  signupPasswordInput.value('');
  signupConfirmPasswordInput.value('');
  signupCharacterClassInput.value('');
  signupCharacterRaceInput.value('');
  signupCharacterInfoInput.value('');

  setTimeout(() => {
    currentScreen = "login";
    displayContent();
    loginMessage.html('New account created. Please log in.');
    loginMessage.style('color', '#00ffaa');
  }, 1500);
}
Line-by-line explanation (7 lines)

🔧 Subcomponents:

initialization Collect All Form Inputs const username = signupUsernameInput.value(); const password = signupPasswordInput.value(); const confirmPassword = signupConfirmPasswordInput.value(); const characterClass = signupCharacterClassInput.value(); const characterRace = signupCharacterRaceInput.value(); const characterInfo = signupCharacterInfoInput.value();

Extract all six pieces of information the player entered in the signup form

conditional Check All Fields Filled if (!username || !password || !confirmPassword || !characterClass || !characterRace || !characterInfo) { signupMessage.html('All fields are required.'); signupMessage.style('color', '#ff3333'); return;

If any field is empty, show an error and exit early without creating an account

conditional Check Passwords Match if (password !== confirmPassword) { signupMessage.html('Passwords do not match.'); signupMessage.style('color', '#ff3333'); return;

Ensure the player typed the same password twice by comparing them

conditional Check Username Uniqueness if (registeredUsers.some(u => u.username === username)) { signupMessage.html('Username already taken.'); signupMessage.style('color', '#ff3333'); return;

Use .some() to check if ANY existing user has this username; if so, reject the signup

initialization Add New User to Database registeredUsers.push({ username, password, gold: 500, characterClass, characterRace, characterInfo }); saveUsers();

Create a new user object with all the signup data and add it to the registeredUsers array, then persist it to localStorage

initialization Reset Form Fields signupUsernameInput.value(''); signupPasswordInput.value(''); signupConfirmPasswordInput.value(''); signupCharacterClassInput.value(''); signupCharacterRaceInput.value(''); signupCharacterInfoInput.value('');

Clear all input fields after successful signup so the form is empty for the next visitor

if (!username || !password || !confirmPassword || !characterClass || !characterRace || !characterInfo) {
Check if ANY of the six fields is empty (falsy). The !variable syntax means 'NOT variable', so !username is true if username is empty. If ANY field is missing, reject the signup.
if (password !== confirmPassword) {
The !== operator means 'not equal to'. If the password field and confirm password field don't match exactly, show an error.
if (registeredUsers.some(u => u.username === username)) {
The .some() method returns true if ANY element in the array passes the test. Here, we test if any user has the same username as the one being created. If so, it's already taken.
registeredUsers.push({...});
Create a new user object with shorthand syntax (username, password, etc. expand to username: username, password: password) and add it to the end of the registeredUsers array
gold: 500,
Every new player starts with 500 gold points. This is the initial economy balance.
saveUsers();
Call the saveUsers() function to write the updated registeredUsers array to localStorage so the new account persists
signupUsernameInput.value('');
Set the username input field's value to an empty string, clearing whatever the player typed

aiPostToFeed()

aiPostToFeed() is a utility function that encapsulates the logic for adding any item to the feed with a timestamp. Notice it takes generic parameters (type, content) so it can be used for any kind of post—bounties, messages, group updates, AI posts, etc. The unshift() method and the conditional displayContent() refresh are key: unshift() makes new posts appear at the top, and the conditional refresh creates an instant-feedback experience when viewing the feed.

function aiPostToFeed(type, content) {
  const now = new Date();
  const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  feedItems.unshift({
    id: feedItems.length + 1,
    type: type,
    content: content,
    timestamp: timestamp
  });
  if (currentScreen === "home") {
    displayContent();
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

initialization Get Current Time const now = new Date(); const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;

Create a human-readable timestamp string in HH:MM format (e.g., '14:45') based on the current system time

initialization Add Item to Feed feedItems.unshift({ id: feedItems.length + 1, type: type, content: content, timestamp: timestamp });

Create a new feed item object and add it to the BEGINNING of the feedItems array (unshift adds to the front, not the end) so new posts appear at the top

conditional Refresh Feed if Visible if (currentScreen === "home") { displayContent(); }

If the player is currently viewing the home/feed screen, immediately refresh the display to show the new post. If they're on another screen, the post is added but not shown until they return to home.

const now = new Date();
Create a JavaScript Date object representing the current moment in time
const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
Extract the hours and minutes from the Date object, convert them to strings, and use padStart(2, '0') to ensure they're two digits (so '5' becomes '05'). Template literals (backticks) combine them into 'HH:MM' format.
feedItems.unshift({...});
Create a new feed item object and add it to the BEGINNING of the feedItems array using unshift(). This makes new posts appear at the top of the feed.
if (currentScreen === "home") { displayContent(); }
If the player is currently looking at the home screen, call displayContent() to re-render the feed immediately so they see the new post appear instantly. If they're on another screen, the post is silently added to the array.

handleJoinGroup()

handleJoinGroup() shows how array methods like .find() and .includes() can validate membership. It also demonstrates how to modify shared state (the groups array) and broadcast changes to the feed. The three-part conditional is defensive programming—it checks every requirement before making changes, preventing bugs like null reference errors or duplicate memberships.

🔬 This checks !group.members.includes(currentUser) to prevent duplicate membership. What happens if you remove this check? A player could theoretically join the same group multiple times—their name would appear twice in the members list. Try removing !group.members.includes(currentUser) and see if it breaks anything.

  if (group && currentUser && !group.members.includes(currentUser)) {
    group.members.push(currentUser);
    aiPostToFeed("group_update", `${currentUser} has joined ${groupName}!`);
function handleJoinGroup(groupId, groupName) {
  const group = groups.find(g => g.id === groupId);
  if (group && currentUser && !group.members.includes(currentUser)) {
    group.members.push(currentUser);
    aiPostToFeed("group_update", `${currentUser} has joined ${groupName}!`);
    aiOutput(`${currentUser} joined group: ${groupName}`);
    currentScreen = "groups";
    displayContent();
  } else {
    aiOutput("You are already in this group or not logged in.");
  }
}
Line-by-line explanation (4 lines)

🔧 Subcomponents:

calculation Locate Group by ID const group = groups.find(g => g.id === groupId);

Search the groups array for the group with the matching ID

conditional Check Join Eligibility if (group && currentUser && !group.members.includes(currentUser)) {

Ensure the group exists, a user is logged in, and that user is not already a member

initialization Add User to Group group.members.push(currentUser);

Add the current player's username to the group's members array

const group = groups.find(g => g.id === groupId);
Search the groups array for a group matching the provided groupId. If found, store it in group; otherwise, group is undefined.
if (group && currentUser && !group.members.includes(currentUser)) {
Check three things: (1) the group exists, (2) a user is logged in, (3) the current user is NOT already in the group's members array. All three must be true to proceed.
group.members.push(currentUser);
Add the logged-in player's username to the end of the group's members array
aiPostToFeed("group_update", `${currentUser} has joined ${groupName}!`);
Post a group_update message to the feed announcing that the player joined, so everyone sees it

saveUsers()

saveUsers() uses localStorage, which is a built-in browser API for persisting small amounts of data. JSON.stringify() converts JavaScript objects/arrays into text so they can be stored; later, JSON.parse() reverses this. localStorage is limited to about 5-10 MB per domain and is perfect for prototype game saves, user preferences, and settings. In production, you'd save to a secure backend server instead.

function saveUsers() {
  localStorage.setItem('registeredUsers', JSON.stringify(registeredUsers));
  console.log("Users saved to localStorage.");
}
Line-by-line explanation (1 lines)

🔧 Subcomponents:

calculation Convert Array to JSON String JSON.stringify(registeredUsers)

Transform the registeredUsers array into a JSON text string so it can be stored as text in localStorage

initialization Store in Browser Storage localStorage.setItem('registeredUsers', ...)

Write the JSON string to the browser's localStorage under the key 'registeredUsers' so it persists after the page closes

localStorage.setItem('registeredUsers', JSON.stringify(registeredUsers));
Convert the registeredUsers array to a JSON string and save it to localStorage with the key 'registeredUsers'. Browser localStorage persists until explicitly cleared, surviving page refreshes and browser restarts.

loadUsers()

loadUsers() demonstrates the load half of the save/load persistence pattern. It uses Map objects to elegantly merge two user lists without duplicates. The pattern ensures that the Admin account always exists (default) but gets overwritten if a saved version exists. This is defensive programming: the app works with or without saved data.

function loadUsers() {
  const storedUsers = localStorage.getItem('registeredUsers');
  if (storedUsers) {
    let loadedUsers = JSON.parse(storedUsers);

    const defaultUsersMap = new Map(registeredUsers.map(user => [user.username, user]));
    loadedUsers.forEach(user => defaultUsersMap.set(user.username, user));
    registeredUsers = Array.from(defaultUsersMap.values());

    console.log("Users loaded from localStorage:", registeredUsers);
  } else {
    console.log("No users found in localStorage. Using default users.");
  }
}
Line-by-line explanation (6 lines)

🔧 Subcomponents:

initialization Retrieve Stored Data const storedUsers = localStorage.getItem('registeredUsers');

Get the JSON string from localStorage using the key 'registeredUsers'. Returns null if nothing was stored.

calculation Parse JSON to Array let loadedUsers = JSON.parse(storedUsers);

Convert the JSON string back into a JavaScript array of user objects

initialization Merge with Default Users const defaultUsersMap = new Map(registeredUsers.map(user => [user.username, user])); loadedUsers.forEach(user => defaultUsersMap.set(user.username, user)); registeredUsers = Array.from(defaultUsersMap.values());

Combine loaded users with default users (like the Admin account), preferring loaded users for any username conflicts

const storedUsers = localStorage.getItem('registeredUsers');
Retrieve the JSON string stored under the key 'registeredUsers'. If nothing was stored, storedUsers is null.
if (storedUsers) {
Only proceed if data was actually stored in localStorage
let loadedUsers = JSON.parse(storedUsers);
Reverse the JSON stringification: convert the text back into a JavaScript array that we can work with
const defaultUsersMap = new Map(...);
Create a Map (a key-value store) from the default users so we can easily merge and update them
loadedUsers.forEach(user => defaultUsersMap.set(user.username, user));
Loop through each loaded user and add them to the map. If a username already exists (from defaults), it gets overwritten with the loaded version.
registeredUsers = Array.from(defaultUsersMap.values());
Convert the map back into an array of user objects and store it in registeredUsers

randomGroupPost()

randomGroupPost() demonstrates defensive programming with early exits, array randomization with p5.js's random() function, and string interpolation to compose readable messages. It's called every 20 seconds by setInterval() to create the illusion of an active world with background activity. This is a key game design pattern: ambient world events that make an empty game feel alive.

🔬 This picks a random group, checks it has members, then picks a random member from that group. What happens if you remove the second check (if randomGroup.members.length === 0)? Could the randomMember be undefined, causing an error? Try it and watch the browser console for errors.

  const randomGroup = random(groups);
  if (randomGroup.members.length === 0) return;

  const randomMember = random(randomGroup.members);
function randomGroupPost() {
  if (groups.length === 0) return;

  const randomGroup = random(groups);
  if (randomGroup.members.length === 0) return;

  const randomMember = random(randomGroup.members);
  const postMessages = [
    `just completed a small task in the ${randomGroup.name}'s territory.`,
    `found some interesting ruins near the ${randomGroup.name}'s last known location.`,
    `resting at the tavern after a long day with the ${randomGroup.name}.`,
    `preparing for a new mission with ${randomGroup.name}.`,
    `discussing strategy with the ${randomGroup.name}.`,
    `training new recruits for the ${randomGroup.name}.`
  ];
  const randomMessage = random(postMessages);

  if (registeredUsers.some(u => u.username === randomMember)) {
    const now = new Date();
    const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
    partyMessages.push({
      sender: randomMember,
      content: randomMessage,
      timestamp: timestamp
    });
  }

  aiPostToFeed("group_update", `${randomMember} from ${randomGroup.name}: '${randomMessage}'`);
}
Line-by-line explanation (8 lines)

🔧 Subcomponents:

conditional Check Groups Exist if (groups.length === 0) return;

Exit early if there are no groups to post from

calculation Pick Random Group const randomGroup = random(groups);

Use p5.js's random() function to pick a random group from the groups array

conditional Check Group Has Members if (randomGroup.members.length === 0) return;

Exit early if the chosen group has no members to post from

calculation Compose Random Message const randomMessage = random(postMessages);

Pick a random message template from the array and combine it with the group name

initialization Add to Feed and Chat if (registeredUsers.some(u => u.username === randomMember)) { ... } aiPostToFeed(...)

Add the message to party comms chat if the member is a real user, and always post to the main feed

if (groups.length === 0) return;
Check if there are any groups. If the groups array is empty, exit the function immediately—there's nothing to post about.
const randomGroup = random(groups);
Use p5.js's random() function to pick a random element from the groups array. This is called a setInterval timer, creating posts every 20 seconds.
if (randomGroup.members.length === 0) return;
Check if the randomly chosen group has any members. If not, exit—we can't have a member post if there are no members.
const randomMember = random(randomGroup.members);
Pick a random username from the chosen group's members array
const postMessages = [...];
An array of message templates that reference the group name. Each message describes a group activity.
const randomMessage = random(postMessages);
Pick one of these templates at random
if (registeredUsers.some(u => u.username === randomMember)) {
Check if this random member is actually a registered user (not an NPC). If yes, add their post to the party comms chat; if no, just post to the main feed.
aiPostToFeed("group_update", `${randomMember} from ${randomGroup.name}: '${randomMessage}'`);
Add the post to the main feed with type 'group_update' so it appears with a group icon

📦 Key Variables

currentScreen string

Stores which screen should be displayed: 'cover', 'login', 'signup', 'home', 'marketplace', 'groups', 'bounties', 'comms', 'members', etc. Changing this and calling displayContent() transitions between screens.

let currentScreen = "cover";
currentUser string

Stores the username of the player currently logged in, or null if no one is logged in. Used to display the HUD and control access to features.

let currentUser = null;
currentUserGold number

Stores how much gold the logged-in player has. Updated when they accept bounties, receive admin gifts, or spend money. Displayed in the HUD.

let currentUserGold = 0;
registeredUsers array

An array of user objects. Each object has username, password, gold, characterClass, characterRace, characterInfo. This is the 'database' for the app.

let registeredUsers = [{username: 'Admin', password: 'Password', gold: 9999, ...}];
feedItems array

An array of post objects that appear in the Party Feed. Each has id, type (bounty_accepted, item_found, etc.), content, and timestamp.

let feedItems = [{id: 1, type: 'bounty_accepted', content: '...', timestamp: '14:45'}, ...];
partyMessages array

An array of chat messages from players in Party Comms. Each has sender, content, and timestamp. Different from feedItems (feed is announcements, chat is direct player messages).

let partyMessages = [];
groups array

An array of group objects. Each has id, name, and a members array with usernames. Players can join and leave groups.

const groups = [{id: 1, name: 'The Adventuring Party', members: ['Cleric', 'Rogue', ...]}, ...];
bounties array

An array of bounty (quest) objects. Each has id, name, description, and reward. When a player accepts a bounty, it's removed from this array and its reward is added to their gold.

const bounties = [{id: 1, name: 'Clear the Goblin Den', description: '...', reward: '250 gp'}, ...];
marketplaceItems array

An array of item objects for sale. Each has id, name, description, and price. Players browse but cannot buy (this is a prototype).

const marketplaceItems = [{id: 1, name: 'Potion of Healing', description: '...', price: '50 gp'}, ...];

🔧 Potential Improvements (5)

Here are some ways this code could be enhanced:

BUG handleAcceptBounty() and handleDenyBounty()

The bounties array is never initialized in the global scope, only used. If displayContent('bounties') is called before any bounty is generated, referencing bounties will throw a ReferenceError.

💡 Add a global initialization at the top: const bounties = []; Then in displayContent(), the 'bounties' case should initialize it with aiGenerateBounty() if it's empty, which it already does—but the initial declaration is missing.

PERFORMANCE displayContent()

Every time displayContent() is called, the function creates new DOM elements (feedDiv, itemDiv, groupDiv, etc.) by looping through arrays and appending to contentDiv. If there are hundreds of feed items, this becomes very slow. These elements are never reused.

💡 Implement a simple virtual scrolling or pagination system. Show only the first 20 feed items and add a 'Load More' button. Or cache DOM elements and update their content instead of recreating them every frame.

STYLE handleLogin() and handleSignup()

Validation logic is duplicated across multiple functions. The checks for empty fields, password matching, and username uniqueness are similar patterns repeated in different places.

💡 Extract validation into reusable helper functions: validateNotEmpty(), validatePasswordMatch(), validateUsernameUnique(). This reduces code duplication and makes validation rules easier to maintain.

FEATURE Bounties and Marketplace

Players can view and accept bounties, but there's no actual 'quest log' or way to see in-progress bounties. Once a bounty is accepted, it disappears—players don't know which ones they're working on.

💡 Add a global array const acceptedBounties = []; When a player accepts a bounty, move it to acceptedBounties instead of deleting it. Add a 'Quest Log' screen showing bounties in progress and completed bounties.

BUG handleRenameUser()

When a user is renamed, the currentUser variable is updated if the logged-in user is renamed. However, if that user is viewing a detail page (marketplace_detail, groups_detail, etc.), the displayed item references that still contain the old username are not updated until displayContent() is called again.

💡 After renaming, ensure displayContent() is called to refresh all views. This is already done at the end of handleRenameUser(), but consider adding a more explicit 'reload detail view' function for consistency.

🔄 Code Flow

Code flow showing setup, displaycontent, handlelogin, handlesignup, aiposttofeed, handlejoingroup, saveusers, loadusers, randomgrouppost

💡 Click on function names in the diagram to jump to their code

graph TD start[Start] --> setup[setup] setup --> draw[draw loop] draw --> canvas-setup[canvas-setup] draw --> load-users[load-users] draw --> create-cover-screen[create-cover-screen] draw --> hide-all[hide-all] draw --> hud-check[hud-check] draw --> position-calculation[position-calculation] draw --> admin-split-screen[admin-split-screen] draw --> feed-rendering[feed-rendering] draw --> screen-switch[screen-switch] click setup href "#fn-setup" click draw href "#fn-draw" click canvas-setup href "#sub-canvas-setup" click load-users href "#sub-load-users" click create-cover-screen href "#sub-create-cover-screen" click hide-all href "#sub-hide-all" click hud-check href "#sub-hud-check" click position-calculation href "#sub-position-calculation" click admin-split-screen href "#sub-admin-split-screen" click feed-rendering href "#sub-feed-rendering" click screen-switch href "#sub-screen-switch" canvas-setup -->|Initializes| load-users load-users -->|Retrieves| create-cover-screen create-cover-screen -->|Builds| hide-all hide-all -->|Ensures| hud-check hud-check -->|Checks| position-calculation position-calculation -->|Calculates| admin-split-screen admin-split-screen -->|Displays| feed-rendering feed-rendering -->|Loops| screen-switch handlelogin[handlelogin] --> get-inputs[get-inputs] get-inputs --> find-user[find-user] find-user --> success-check[success-check] success-check -->|Validates| admin-check[admin-check] admin-check -->|Shows| start-intervals[start-intervals] start-intervals -->|Begins| draw click handlelogin href "#fn-handlelogin" click get-inputs href "#sub-get-inputs" click find-user href "#sub-find-user" click success-check href "#sub-success-check" click admin-check href "#sub-admin-check" click start-intervals href "#sub-start-intervals" handlesignup[handlesignup] --> collect-inputs[collect-inputs] collect-inputs --> validate-empty[validate-empty] validate-empty --> validate-password-match[validate-password-match] validate-password-match --> check-username-unique[check-username-unique] check-username-unique --> create-user-object[create-user-object] create-user-object --> clear-inputs[clear-inputs] click handlesignup href "#fn-handlesignup" click collect-inputs href "#sub-collect-inputs" click validate-empty href "#sub-validate-empty" click validate-password-match href "#sub-validate-password-match" click check-username-unique href "#sub-check-username-unique" click create-user-object href "#sub-create-user-object" click clear-inputs href "#sub-clear-inputs" aiposttofeed[aiposttofeed] --> add-to-feed[add-to-feed] add-to-feed --> refresh-if-home[refresh-if-home] click aiposttofeed href "#fn-aiposttofeed" click add-to-feed href "#sub-add-to-feed" click refresh-if-home href "#sub-refresh-if-home" handlejoingroup[handlejoingroup] --> find-group[find-group] find-group --> validate-join[validate-join] validate-join --> add-member[add-member] click handlejoingroup href "#fn-handlejoingroup" click find-group href "#sub-find-group" click validate-join href "#sub-validate-join" click add-member href "#sub-add-member" saveusers[saveusers] --> stringify[stringify] stringify --> store[store] click saveusers href "#fn-saveusers" click stringify href "#sub-stringify" click store href "#sub-store" loadusers[loadusers] --> retrieve[retrieve] retrieve --> parse[parse] parse --> merge[merge] merge -->|Merges| draw click loadusers href "#fn-loadusers" click retrieve href "#sub-retrieve" click parse href "#sub-parse" click merge href "#sub-merge" randomgrouppost[randomgrouppost] --> early-exit-1[early-exit-1] early-exit-1 --> pick-group[pick-group] pick-group --> early-exit-2[early-exit-2] early-exit-2 --> pick-message[pick-message] pick-message --> post-to-feed[post-to-feed] click randomgrouppost href "#fn-randomgrouppost" click early-exit-1 href "#sub-early-exit-1" click pick-group href "#sub-pick-group" click early-exit-2 href "#sub-early-exit-2" click pick-message href "#sub-pick-message" click post-to-feed href "#sub-post-to-feed"

❓ Frequently Asked Questions

What visual elements does the Civilization sketch showcase?

The sketch creates a vibrant fantasy marketplace featuring various items, groups, and bounties, visually representing a rich game world.

How can users interact with the Civilization sketch?

Users can explore the marketplace items, view group members, and check available bounties, enhancing their engagement with the content.

What creative coding techniques are highlighted in the Civilization sketch?

The sketch demonstrates the use of mock data to simulate a gaming environment, showcasing object-oriented programming and data-driven design in p5.js.

Preview

Civilization - p5.js creative coding sketch preview
Sketch Preview
Code flow diagram showing the structure of Civilization - Code flow showing setup, displaycontent, handlelogin, handlesignup, aiposttofeed, handlejoingroup, saveusers, loadusers, randomgrouppost
Code Flow Diagram