Boximus the Adventure Master

by julianvisser in Circuits > Arduino

107 Views, 1 Favorites, 0 Comments

Boximus the Adventure Master

20220616_172053.jpg

In my spare time I love to play table-top adventure games such as D&D, navigating long campaigns and fighting fantasy monsters. Unfortunately it's always hard for anyone to find a game master, or a reliable group to play with. One could settle for single-player adventure games, but those lack a certain physicality and sociality. For this reason I built an object which will run as many fantasy adventures as I wish for me. Boximus the Adventure Master is like Matt Mercer encased in MDF-board.

While this might be a slight exaggeration, Boximus will generate, narrate, and dictate the outcome of a simple but suspenseful adventure game, on the fly and as many times as you wish. Every adventure will be different!

This project describes how I built a box which plays a rudimentary 'text adventure' type game with a player, narrating the game via an MP3 player module and speaker, and accepting input via three buttons.

Supplies

Components used:

  • Arduino Uno
  • MP3-TF-16P V3.0
  • SD Card
  • 4 Ohm 3 Watt Adafruit speaker
  • Push button (3)
  • Jumper wires
  • Perfboard
  • USB-A to USB-B cable
  • USB powerbank

For the housing:

  • MDF Board
  • Glue
  • White pencil

For assembly of the electronics and the housing, soldering equipment and laser cutter were used respectively.

Concept

ittt-sketch1.png

The idea for this project stems for my appreciation for both table-top RPGs and classic text-based adventure games, where the computer narrates a location or situation and the player inputs a simple command to progress. I estimated it would be fun to make something that would actually speak to the user, and play a game with them.

As is visible in this sketch, my original idea for a 'dungeon master bot' included features such as a controller and arms which could throw dice around with reckless abandon. For the sake of time, I decided to cut any unnecessary flourishes and focus on making the core concept work.

The final version of this concept is a playable, simple 'adventure' game that is narrated to the player via audio and can be controlled via directional buttons.

Planning

ittt-sketch2 (1).png

This diagram shows roughly how I planned out the technical aspect of the project. The player inputs decide which way to go, then the program generates new locations as options. These are then announced by playing several pre-recorded audio files with the DFPlayer and speaker. Then, the player can input which way to go again. This loop can be interrupted by a 'fight' encounter, where the player will either gain gold or game over.

In this diagram, there are still four directional buttons, one for north, east, south, and west. I decided that these could be simplified to two, and made more intuitive by simply presenting the player with a 'left' and 'right' option, in addition to the option to return to their previous location.

Testing Electronics

circuit.png
20220517_153636.jpg

Next, I tested all the necessary components on a breadboard. This includes the Arduino, DFPlayer, SD card, speaker, buttons, and the wires to connect them. This step undoubtedly gave me the most trouble. I spent weeks troubleshooting problems with the speaker and DFPlayer, until I finally found out I needed to replace my DFPlayer module, as apparently there was no way for the faulty module I had bought to read the names or IDs of the audio files. Specifically, I replaced an MP3-TF-16P module for an MP3-TF-16P V3.0. Be sure to use the latter if reproducing this project.

After great time and effort all components were working and assembled as seen in the schematic. It's a relatively simple circuit to build, but not without its obstacles, such as needing to connect both the 5V and ground pins to multiple components, and positioning the multiple resistors correctly.

Code

With the electronics operable, I could control them via code. As a basis, I took this sample code for operating a DFPlayer, which uses a library to send commands to the module instructing it which files to play.

With a function such as 'playMp3Folder()' the DFPlayer can be given the name of a specific audio file to play, provided the name of the file starts with a four-digit number.

As soon as I was able to play the audio files I wanted on command, I was able to start programming a gameplay loop. I made a function that checks for inputs via the buttons, a function that outputs the current 'locations', a function that deals with the enemy encounters, a function to generate new options for locations, and a function to restart the game after it ends. A simple game like this can be constructed from a somewhat organised arrangement of different if-statements.

My complete code can be found here:

// Portions of code used from: <a href="https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR0299" rel="nofollow"> https://wiki.dfrobot.com/DFPlayer_Mini_SKU_DFR029...</a>
// This code allows you to play an audio-based adventure game, by playing different audio files.
// The files are numbered with 4-digit numbers, so that the DFPlayer can read them. 
// Audio files 0001-0010 describe outside locations, audio files 0011-0020 describe inside 
// locations, and audio files 0021-0030 describe creature encounters in ascending order of 
// difficulty. 31-42 are miscellaneous lines which announce various things in the game.
// The game can also be played via the text printed to the SoftwareSerial.

#include "Arduino.h"
#include "SoftwareSerial.h"
#include "DFRobotDFPlayerMini.h"

SoftwareSerial mySoftwareSerial(10, 11); // RX, TX
DFRobotDFPlayerMini myDFPlayer;
void printDetail(uint8_t type, int value);


bool gameActive = false;
bool waitingForInput = false;

bool leftPressed = false;
bool backPressed = false;
bool rightPressed = false;

int currentLoc = 0;
int leftLoc = 0;
int rightLoc = 0;
int backLoc = 0;

int gold = 0;

long timer = 0;

void setup()
{
  pinMode(2, INPUT); // Left button
  pinMode(3, INPUT); // Back button
  pinMode(4, INPUT); // Right button
  
  mySoftwareSerial.begin(9600);
  Serial.begin(115200);

  Serial.println();
  Serial.println(F("DFRobot DFPlayer Mini Demo"));
  Serial.println(F("Initializing DFPlayer ... (May take 3~5 seconds)"));

  if (!myDFPlayer.begin(mySoftwareSerial)) {  // Use softwareSerial to communicate with mp3.
    Serial.println(F("Unable to begin:"));
    Serial.println(F("1.Please recheck the connection!"));
    Serial.println(F("2.Please insert the SD card!"));
    while(true);
  }
  Serial.println(F("DFPlayer Mini online."));

  myDFPlayer.volume(29);  //Set volume value. From 0 to 30

  randomSeed(analogRead(0));  
  // Use random noise from the unconnected analog pin 0 to generate a random seed every time
  // Without this, the 'encounters' would be the same each time the program is reset

  delay(6000);  //Delay so the DFPlayer module has time to start up
}

void loop()
{
  handleInput();

  if(millis() - timer > 60000){ 
    // waitingForInput = false; // Voids the opportunity for an input after 60 seconds of inaction and repeats the introduction
  }
  
  if(!gameActive){ 
    Serial.println("Startgame activated"); 
    startGame();
  }
  
  if(gameActive && !waitingForInput && leftPressed){
    if(leftLoc > 20){
      handleEncounter(leftLoc);
      leftLoc = random(1, 11);
    } else {
    backLoc = currentLoc;
    currentLoc = leftLoc;
    generateNewLocs(); 
    }
  }

  if(gameActive && !waitingForInput && backPressed){
    currentLoc = backLoc;
    generateNewLocs();
  }

  if(gameActive && !waitingForInput && rightPressed){
    if(rightLoc > 20){
      handleEncounter(rightLoc);
      rightLoc = random(1, 11);
    } else {
    backLoc = currentLoc;
    currentLoc = rightLoc;
    generateNewLocs();
    }
  }
  
  if(gameActive && !waitingForInput){
    introduceLocations();
    waitingForInput = true;
    timer = millis();    
  }

  leftPressed = false;
  backPressed = false;
  rightPressed = false;

  if (myDFPlayer.available()) {
    printDetail(myDFPlayer.readType(), myDFPlayer.read()); // Print the detail message from DFPlayer to handle different errors and states.
  }
}

void generateNewLocs(){
  leftLoc = random(1, 21); 
    rightLoc = random(1, 21);
    if(random(1, 3)==2){
      leftLoc = random(21, 31);
    }
    if(random(1, 3)==2){
      rightLoc = random(21, 31);
    }
}

void handleEncounter(int difficulty){
  Serial.print(F("Encounter being handled:"));
  Serial.println((difficulty));

  myDFPlayer.playMp3Folder(35); // You prepare to fight the creature!
  Serial.println(F("You prepare to fight the creature!"));
  delay(8000);
    
  int roll = random(21, 34); // 'Rolls' for a number 21-31 to compare to difficulty 21-30
  if(roll > difficulty){
    gold = gold + ((difficulty - 17) * 3);
    
    myDFPlayer.playMp3Folder(36); // You slay the creature and steal its gold!
    Serial.println(F("You slay the creature and steal its gold!"));
    delay(8000);
  } else {
    gameActive = false;
    
    myDFPlayer.playMp3Folder(37); // The creature slew you! You are dead!
    Serial.println(F("The creature slew you! You are dead!"));
    delay(8000);

    if(gold > 100){
      myDFPlayer.playMp3Folder(38); // Incredible!! You collected over 100 gold!
      Serial.println(F("Incredible!! You collected over 100 gold!"));
      delay(8000);
    } else if(gold > 50){
      myDFPlayer.playMp3Folder(39); // Wow! You collected over 50 gold!
      Serial.println(F("Wow! You collected over 50 gold!"));
      delay(8000);
    } else if(gold > 20){
      myDFPlayer.playMp3Folder(40); // Wow! You collected over 20 gold!
      Serial.println(F("Wow! You collected over 20 gold!"));
      delay(8000);
    } else if(gold > 10){
      myDFPlayer.playMp3Folder(41); // Well done! You collected over 10 gold!
      Serial.println(F("Well done! You collected over 10 gold!"));
      delay(8000);
    }
  }
}



void handleInput(){
  if(waitingForInput){
    if(digitalRead(2) == 1){
      leftPressed = true;
      waitingForInput = false;
    }
    
    else if(digitalRead(3) == 1){
      backPressed = true;
      waitingForInput = false;
    }
    
    else if(digitalRead(4) == 1){
      rightPressed = true;
      waitingForInput = false;
    }
  }
}



void introduceLocations(){
  myDFPlayer.playMp3Folder(31); // You are in...
  Serial.println(F("Narrator: You are in..."));
  delay(4500);
  
  myDFPlayer.playMp3Folder(currentLoc); // Current location
  Serial.print(F("Narrator: Current location: "));
  Serial.println((currentLoc));
  delay(6500);


  myDFPlayer.playMp3Folder(32); // To your left is...
  Serial.println(F("Narrator: To your left is..."));
  delay(4500);

  myDFPlayer.playMp3Folder(leftLoc); // Left location
  Serial.print(F("Narrator: Left location: "));
  Serial.println((leftLoc));
  delay(6500);

  myDFPlayer.playMp3Folder(33); // To your right is...
  Serial.println(F("Narrator: To your right is..."));
  delay(4500);

  myDFPlayer.playMp3Folder(rightLoc); // Right location
  Serial.print(F("Narrator: Right location: "));
  Serial.println((rightLoc));
  delay(6500);

  myDFPlayer.playMp3Folder(34); // Where do you wish to go?
  Serial.println(F("Narrator: Where do you wish to go?"));
  delay(5000);
}



void startGame(){
  myDFPlayer.playMp3Folder(42); // Welcome. You have embarked on an adventure in search of gold.
  Serial.println(F("Welcome. You have embarked on an adventure in search of gold."));
  delay(11000);

  currentLoc = random(1, 21);
  backLoc = currentLoc;
  leftLoc = random(1, 31);
  rightLoc = random(1, 31);

  gold = 0;
  
  gameActive = true;
}

void printDetail(uint8_t type, int value){ 
  switch (type) {
    case TimeOut:
      Serial.println(F("Time Out!"));
      break;
    case WrongStack:
      Serial.println(F("Stack Wrong!"));
      break;
    case DFPlayerCardInserted:
      Serial.println(F("Card Inserted!"));
      break;
    case DFPlayerCardRemoved:
      Serial.println(F("Card Removed!"));
      break;
    case DFPlayerCardOnline:
      Serial.println(F("Card Online!"));
      break;
    case DFPlayerPlayFinished:
      Serial.print(F("Number:"));
      Serial.print(value);
      Serial.println(F(" Play Finished!"));
      break;
    case DFPlayerError:
      Serial.print(F("DFPlayerError:"));
      switch (value) {
        case Busy:
          Serial.println(F("Card not found"));
          break;
        case Sleeping:
          Serial.println(F("Sleeping"));
          break;
        case SerialWrongStack:
          Serial.println(F("Get Wrong Stack"));
          break;
        case CheckSumNotMatch:
          Serial.println(F("Check Sum Not Match"));
          break;
        case FileIndexOut:
          Serial.println(F("File Index Out of Bound"));
          break;
        case FileMismatch:
          Serial.println(F("Cannot Find File"));
          break;
        case Advertise:
          Serial.println(F("In Advertise"));
          break;
        default:
          break;
      }
      break;
    default:
      break;
  }
}<br>

Recording and Implementing Audio

This is the fun part! In order for the game to work and the device to speak to the user, I had to record voice lines for it to say. I put on my best imposing dungeon master voice and recorded voice lines announcing all the different locations, and the things to say in between. I used my phone to record my voice, and to put the audio files onto the SD card. At first the DFPlayer did not recognise the mp3 files I had created, but I was able to remedy this issue by running the files through fConvert, a free online file converter, making sure to export the files in 'mono' audio: With a single audio channel. This didn't take me too long.

Included below is the full list of lines which I recorded, by number. As mentioned in the comments in the code, there are 20 locations, then 10 creatures, then 12 miscellaneous lines to go between them.

With the electronics, code and audio working, the only thing left to do is actually construct the device and make it presentable.

<p>1. "a grassy meadow"</p><p>2. "a lush forest"</p><p>3. "the snowy peaks of a mountain"</p><p>4. "a barren cliffside"</p><p>5. "a monolithic jungle"</p><p>6. "the ruins of an ancient city"</p><p>7. "a small hamlet on a mountainside"</p><p>8. "a valley full of giant bones"</p><p>9. "the foot of a raging volcano"</p><p>10. "a foreboding graveyard"</p><p>11. "an evil wizard's tower"</p><p>12. "a nobleman's abandoned castle"</p><p>13. "a temple of shiny marble"</p><p>14. "the remains of a giant's citadel"</p><p>15. "a camp set up by orc nomads"</p><p>16. "a cave filled with icy pillars"</p><p>17. "a gargantuan hollow tree"</p><p>18. "a dusty stronghold under a hill”</p><p>19. "a damp and rumbling cave"</p><p>20. “a vampire’s mausoleum”</p><p>21. "a cowering goblin"</p><p>22. "a gnoll ridden by a gnome"</p><p>23. "a hideous giant bat"</p><p>24. "the black knight"</p><p>25. "a snarling six-foot-tall porcupine"</p><p>26. "a slippery ice golem"</p><p>27. "a shrieking translucent ghost"</p><p>28. "an evil thunder-summoning wizard"</p><p>29. "a giant rock golem"</p><p>30. "an immortal lich”</p><p>31. You are at…</p><p>32. To your left is…</p><p>33. To your right is…</p><p>34. Where do you wish to go?</p><p>35. You prepare to fight the creature!</p><p>36. You slay the creature and steal its gold!</p><p>37. The creature slew you! You are dead!</p><p>38. Incredible!! You collected over 100 gold!</p><p>39. Wow! You collected over 50 gold!</p><p>40. Wow! You collected over 20 gold!</p><p>41. Well done! You collected over 10 gold!</p><p>42. Welcome. You have embarked on an adventure in search of gold.</p>

Soldering and Assembly

20220616_172921.jpg
20220616_173702.jpg
20220607_113818.jpg

I soldered together the electronics on a perfboard. I soldered the DFPlayer directly onto the perfboard, as I knew I was not likely to reuse it elsewhere. To be able to reuse the DFPlayer without dismantling it from the solder and perfboard, some header pins would be useful.

One thing I regretted is using wires that were too short and awkward to reach the pins of the Arduino and the push buttons. If reproducing this project, be sure to use wires that are long enough to connect all your components. I ended up having to solder the ends of wires together to reach the buttons.

For the 'case', I laser-cut some black MDF-board. I used MakerCase to very easily generate a dxf file for the laser cutter to read. The website generates a vector drawing of six different parts that fit together to make a box, then the laser cutter can cut along the lines of the drawing. I used the free software LibreCAD to edit the drawing to include holes for the speaker and three buttons.

I used glue to fasten the parts of the box together, and to secure the speaker to the front of the box. Attaching the speaker via pre-made screw holes would probably be wiser, making it easier to remove. To attach the perfboard with electronics, as well as the powerbank I used, to the inside of the box, I simply used painter's tape.

Final Touches

20220616_172053.jpg
20220616_172106.jpg

The great Boximus has been assembled!

In any game, it is courteous to include a good user interface. I used a simple white pencil to label the direction buttons with road signs, and added a space for high scores to be written down.

Given more time I would have decorated the project more elaborately, but all essential elements are here and using it is quite fun!