Roguelike Pumpkin Patch Documentation

Roguelike Pumpkin Patch is a library for building roguelikes that run in your browser, written in TypeScript or JavaScript. This documentation is meant to show you how to actually use the library. Check out the readme here for setup instructions!

The Display

The display is where most of the magic happens. Contains a multitude of tiles, and is based on HTML and CSS. Suppose you have the following as your CSS:


/* You can style the tiles with pumpkin-tile! You can also target pumpkin-display and pumpkin-container */
.pumpkin-tile {
    font-family: monospace;
}

/* The display will fit the container you put it in; make sure it has a size! */
.pumpkin-container {
    height: 400px;
}

/* Just some styles that you think look cool! */
.brickWall {
    background: darkred;
    color: gray;
}

.player {
    color: white;
}

.coolFloor {
    color: green;
}

.superAwesome {
    background: purple;
    color: white;
    font-family: sans-serif;
}

                

Usage of the display might look like:


// First, select the target element you want the display to be within
const target = document.getElementById("displayExample");

// Paramaters object
const params = {
    // Required! The display must go somewhere
    target: target,
    // Width of the display in tiles
    width: 20,
    // Height of the display in tiles
    height: 15,
};

// Start the display!
const display = new Display(params);

// Set the tile size so that it fits its container
display.tileSize = display.calculateTileSize();

// One cool thing you can do is add a listener for window resizing
// Keep your display looking good!
window.addEventListener("resize",()=>{
    display.tileSize = display.calculateTileSize();
});

// Lets draw some stuff
// Note, x and y are relative to the top left corner!
for (let x=0; x < params.width; x++) {
    for (let y=0; y < params.height; y++) {
        // Draw some walls
        if (x===0 || y===0 || x===params.width-1 || y===params.height-1) {
            display.setTile(x,y,{
                // Content can be a string, or ANY html element! (including images!)
                content: '#',
                className: "brickWall"
            });

        // Add our player!
        } else if (x===3 && y===5) {
            display.setTile(x,y,{
                content: '@',
                // You can use as many classes as you would like!
                classList: ["player"]
            });
        // Some floor everywhere else
        } else {
            display.setTile(x,y,{
                content: '.',
                className: "coolFloor"
            });
        }
    }
}

// Hmm actually I want to change some of the tiles a bit. updateTile changes the
// parameters that you specify; setTile replaces everything.

for (let i=5; i < 10; i++) {
    display.updateTile(i,i,{className:"superAwesome"});
}

                

The results will be:

There are some other methods you might enjoy. If you want to center the display on a particular location (i.e. the player's position, or a security camera, or a cutscene, etc), you can use centerDisplay:


display.centerDisplay(x,y);
                

If you want to automatically determine how many tiles you need to fill the available space, given whatever size they currently are, you can use calculateDimensions:


display.dimensions = display.calculateDimensions();
                

If you want to do something more in depth with a particular tile, you can use getTile to access it:


const tile = display.getTile(x,y);
                

Lastly, while the default style for the display is a black background with white foreground, this is handled by CSS inserted into the pages head; you can just override it, and set any other styles you'd like, with your own stylesheet by accessing pumpkin-container. The only details that are handled inline are the font size, tile size, and tile position, and you can use the tileSize setter to change the first two.


.pumpkin-container.colorDisplay {
    background: linear-gradient(#FF00FF, #FFFF00);
    color: darkslateblue;
    font-weight: bold;
}
                

// Select the element we want to be the target.
const colorTarget = document.querySelector(".colorDisplay");

// Initialize the colorful display
const colorDisplay = new Display({target:colorTarget, width: 10, height: 10});

// Draw some stuff
for(let i=0;i < 10;i++) {
    for(let j=0;j < 10;j++) {
        if (i===0 || j===0 || i===9 || j===9) {
            // Did you know you can use a shorthand here? Now you do!
            colorDisplay.setTile(i,j,'#');
        } else if (i===3 && j===3) {
            colorDisplay.setTile(i,j,'@');
        } else if (i===4 && j===5) {
            colorDisplay.setTile(i,j,{
                content: 'g',
                // You can use inline colors and backgrounds if
                // you REALLY want to, but I discourage it.
                color: 'green',
                background: 'rgba(128,0,0,0.2)',
            });
        } else {
            colorDisplay.setTile(i,j,'.');
        }
    }
}

// Make the display fit the container
colorDisplay.tileSize = colorDisplay.calculateTileSize();

// Center it on the player
colorDisplay.centerDisplay(3,3);
                

Psychadelic!

Random Numbers, and related utilities

Generating random numbers is fun for the whole family. Here's how you do it.

                    
// You can define a seed if you would like! If not, the generator figures out its own.
const optionalSeed = Date.now();

// Start the random number generator
const random = new Random(optionalSeed);

// If you want a random number from 0 <= x < 1:
const x = random.getRandom();

// If you would like a random number in the range of lower <= y <= upper you can use getNumber.
const lower = 0;
const upper = 10;
const y = random.getNumber(lower,upper);

// If the given bounds are integers, it will generate integers.
// If not, it will generate decimals.
// If you want to specify explicitly, use the integer boolean parameter.
const noThanksNotInteger = random.getNumber(lower,upper,false);

// If you want a random element from an array, use getRandomElement.
const coolArray = [1, 2, 3, 4, 5, 6, 7];
const randomElement = random.getRandomElement(coolArray);

// If you want to provide weights for each value, you can use getWeightedElement.
const weightedArray = [
    {
        weight: 10,
        option: "Cute dog"
    },
    {
        weight: 15,
        option: "Awesome cat"
    },
    {
        weight: 1,
        option: "Rare Franklin"
    }
];

const randomWeightedElement = random.getWeightedElement(weightedArray);
                

Here's some results:

The random number generator uses an implementation of the middle square Weyl sequence RNG, described in Widynski (2017).

Field of View

It's nice to see things. Roguelike Pumpkin Patch lets you do it.


// Here's a cool map to live in
const map = [
    "####################",
    "#..................#",
    "#..#.....#....#....#",
    "#..#.....###...##..#",
    "#..#.....#.........#",
    "#.............####.#",
    "#........#....#....#",
    "####...###....#....#",
    "#........#....#....#",
    "####################",
];
const width = map[0].length;
const height = map.length;

// And lets start up a display to use
const fovDisplayParams = {
    target: document.getElementById("fovMap"),
    width: width,
    height: height,
};
const fovDisplay = new Display(fovDisplayParams);

// FOV takes a callback function that decides whether or not you can see something.
// It takes one parameter, which is a two element position array pos = [x,y]
// and returns true or false.
const canSee = (position) => {
    const x = position[0];
    const y = position[1];

    // Make sure it's even on the map
    if ( x<0 || x>=width || y<0 || y>=height) {
        return false;
    }

    const tile = map[y][x];

    // First, regardless of success or not, see this tile
    fovDisplay.setTile(x,y,tile);

    // Next, use whatever criteria we want to decide if it is seethrough or not.
    // In this case, if it's not a wall (or # character), we can see through it.
    return tile !== '#';
}

// It has an optional second parameter for distance. The default is 8.
// Be careful not to set this too high though.
const optionalRange = 20;

// Initialize the FOV object!
const fov = new FOV(canSee, optionalRange);

// Choose a position for the player to be
const playerPos = [5,5];

// Slightly hacky way to add the player; use a better data structure for your games!
const mapRow = map[playerPos[1]];
map[playerPos[1]] = mapRow.slice(0,playerPos[0]) + '@' + mapRow.slice(playerPos[0]);

// Now, LOOK!
fov.look(playerPos);                    
                

FOV currently uses a shadow casting algorithm. It works well in most cases, but be careful not to set the range too high, especially if your game has large outdoor areas.

The Events System

Events are cool. Actions are cool. How do you make them happen? Why, with the EventManager, of course!


// First, some setup, so we can record our output
const simpleList = document.getElementById("simpleEventList");
const complexList = document.getElementById("complexEventList");


// Lets make a helper function to record our output
const showAction = (action, list) => {
    // Make a list item and add the action to it.
    const listItem = document.createElement("li")
    listItem.appendChild(document.createTextNode(action));
    // Attach it to the list, so we can see what happens.
    list.appendChild(listItem);
};

// There's two types of event managers you can make.
// Lets start with the simple one. Everyone takes turns, one after the other.
const simpleEvents = new EventManager({type:"simple"});

// Usually, we want to add some actors to the system.
// The system calls their "act" method.

const simpleGoblin = {
    act: ()=>showAction("The Goblin goblins!", simpleList)
}

const simpleCat = {
    act: ()=>showAction("The cat meows!", simpleList)
}

// Add them to the event manager
simpleEvents.add(simpleGoblin);
simpleEvents.add(simpleCat);

// You can also add callback functions on their own, as events or whatever your heart pleases.
// They can repeat forever...
simpleEvents.add({
    callback: ()=>showAction("Drip drip goes the faucet.", simpleList),
    repeats: true
})

// Repeat a few times
simpleEvents.add({
    callback: ()=>showAction("Rushing wind! Oh no!", simpleList),
    repeats: 2
})

// Or not repeat at all!
simpleEvents.add({
    callback: ()=>showAction("The house of cards falls over. Whoops!", simpleList)
});

// Then you just kick it off in your preferred manner.
// Each time you call advance, it will step forward one step.
// If act returns a promise (say, if you're waiting for player input)
// it will wait for that action to conclude.
for(let i=0;i<20;i++) {
    simpleEvents.advance();
}

// The second type of event manager is complex:
const complexEvents = new EventManager({type:"complex"});

// The complex event manager accepts different delays for different actors

const fastCat = {
    act:()=>showAction("Fast cat nyooms!", complexList)
}

const slowOgre = {
    act:()=>showAction("Slow ogre is sloooow", complexList)
}

// The delay property defines how slow an actor is
complexEvents.add({
    actor:fastCat,
    delay:1
});

complexEvents.add({
    actor:slowOgre,
    delay:5
});

// Or how long an event takes
complexEvents.add({
    callback:()=>showAction("The mail has just arrived. Sweet!", complexList),
    delay: 16
});

// Advance the clock...
for(let i=0;i < 20;i++) {
    complexEvents.advance();
}
                        
                

Simple Event System:

    Complex Event System:

      Pathfinding

      Finding your way from one place to another can be tough. We can help!

      
      // First, lets setup another display! I want to draw the path we find.
      // And lets start up a display to use.
      // We're going to use the same map from the FOV section.
      const pathDisplayParams = {
          target: document.getElementById("pathDisplay"),
          width: width,
          height: height,
      };
      const pathDisplay = new Display(pathDisplayParams);
      pathDisplay.tileSize = pathDisplay.calculateTileSize();
      
      // Lets draw the map to start
      map.forEach((row,y)=>row.split('').forEach((tile,x)=>{
          pathDisplay.setTile(x,y,tile);
      }));
      
      // Now, lets setup the pathfinder!
      // The PathFinder takes a "canPass" callback to determine what is passable.
      // This looks similar to the canSee callback from before, but it doesn't have to.
      const pathfinder = new PathFinder({
          canPass:([x,y])=>{
              // Make sure it's even on the map
              if ( x<0 || x>=width || y<0 || y>=height) {
                  return false;
              }
              const tile = map[y][x];
      
              // Next, use whatever criteria we want to decide if it is passable.
              // In this case, if it's not a wall (or # character), we can walk through it.
              return tile !== '#';
          }
      });
      
      // Let choose a starting position, and a target position!
      const startPos = [1,8];
      const endPos = [15,8];
      
      // It can also take an optional "orthogonalOnly" parameter.
      // This sets whether or not the pathfinder will use diagonals.
      const optionalOrthogonalOnly = false;
      
      // Now, lets find the path!
      const path = pathfinder.findPath(startPos, endPos, optionalOrthogonalOnly);
      
      // Draw it onto the map to take a look at it.
      path.forEach(([x,y])=>{
          pathDisplay.updateTile(x,y,{
              content:'X',
              className: "pathMarker"
          })
      });
      
      // Note that the drawn path doesn't include the starting position.
      // This is so you can just grab path[0] to get the first step in your journey.
      // Lets draw on the player, too, for illustration.
      pathDisplay.updateTile(startPos[0], startPos[1], '@');
                      

      Other cool parameters that you can give the PathFinder include:

      • a weight callback function, to set if certain tiles cost more to walk through (i.e. mud, rocks, mud, monsters...)
      • A custom metric callback function. Currently, the PathFinder uses the A* pathfinding algorithm with the Manhattan metric; you can provide your own metric instead!
      • A maxIterations number. If you want to prevent monsters from calculating extremely large paths, you can prevent that by placing a cap on it.