Game/System Design

Ancient Artifacts

This 2-4 player game sees scientists competing over the acquisition of valuable artifacts while simultaneously battling against a doomsday count down. Clear obstacles and overcome hazards to reach the artifacts that will help the scientists in future acquisition or in hindering the other players. 20-40 minute play time.

Ancient Artifacts is a game about paleontologists and archeologists racing to uncover relics and artifacts as the Doomsday clock encroaches. The primary focuses for game design were player progression, inter-player interaction, and promoting risk-taking behavior.

This game was developed over a 4 month window with iteration and feedback present along the way.

Design Document (7 pages)


Politics in a Nutshell

Constraints: 4 hour time limit, game jam, serious game

This three-player game has players assuming the roles of political parties that wish to get even a single issue into policy. Players must fight against or persuade the public’s opinion (as represented by the Overton window) in order to move their issue up in the political window until it becomes policy – while ideally not allowing others to do the same. Further, each party has an issue that their constituents REALLY do not want as Policy. ~15 minutes of play per rounds.

Politics in a Nutshell is a serious game that is meant to make players critically think about how politics influences the public and vice-versa. Without committing to any political opinions, the game challenges players to put themselves in the shoes of a politician with three goals: move an idea into policy, stop an opposing idea from moving into policy, and be the first to accomplish these tasks.

Players must earn political favor to stay in office, utilize money to shift policy, and try to influence the Overton Window to bring voters to their side. From a game design perspective, the game attempts to use the Three-Player Problem (wherein the two players who are behind will antagonize the leader) as an intended feature.

Design Document (4 pages)

...

Game Start

...

Game End


Pokemon Damage Calculator (Systems Design)

This spreadsheet shows my ability to implement complex systems into a tool that can be used for game designers to balance and test values against one another. Custom moves, types, Pokemon and weather are all modifiable from the useability-first sheet.

Google Sheets link (7 sheets)

...

Example 1

...

Example 2


Gameplay Programming

Goal Oriented Action Planning - Unity Game

Constraints: 1 week time frame

This is my implementation of Goal-Oriented Action Planning, with a small game to go alongside it. There are two types of AI in the game, creatures and humans. Both need to eat and drink to survive, but humans can build structures and chop trees, whereas creatures can nap and poop.

The game itself is a small god game, where the player does not interact directly with the AI, rather they influence their actions in vague ways. Players can place additional creatures, humans, trees and water canisters to change the environment and see how AI behaves.

Github Link
Action Planner Direct Link

...

The game world after humans have created some structures. A creature can be seen napping in a tent and humans chopping trees.


Spell Creator

Constraints: 1 week time frame

The Spell Creator is a player system that I developed roughly based on Morrowind's custom spells. I wanted to demonstrate a new take on the system that allows for customized movement of spells, with support for basic spell effects (such as electric damage). In order to make implementing new spells simpler, I utilized the subclass sandbox pattern. The prototype pattern(ish) was also conceptually useful here, as it allows for simpler copying of a spell. This is used, for example, when splitting a spell into 3, but wanting to retain the effects that follow in the builder for each new spell.

Github Link

...

Spell creator UI. (I'm no artist...)

...

An example custom spell that was cast and has left the play field.


Two Kingdoms - Unity

Constraints: 1 week time frame, Puzzle game, Command pattern

This project was done as a test of the command pattern to implement a undo/redo system for a puzzle game. I chose to create a card battle puzzle, similar to Hearthstone's puzzle game modes. I thought that an undo/redo would be particularly interesting to implement for a game like this, as playing some cards can cause chain effects on the board - meaning a single press of the undo button would need to undo all board changes since the last card played.

The game as a comprehensive tutorial as well!

Github Link
Command Handler direct link

...

The puzzle level


Engine Programming/General CS

Stream Processing Engine

Research Project - C++

The Stream Processing Engine was a project that myself and 6 other students undertook during a research class. The engine is designed to take an endless stream of data, allow operators to 'do work' on them, then provide an end point for said data. One application that we utilized the engine for was modifying live camera data to apply filters or distortion effects. Users first set up input streams (of arbitrary data type), then define operators that work on the data, which then pass potentially modified data to the next operator. Finally, an end point can be defined that allow users to work with the output of the engine in manageable sizes.One of my personal contributions on this project was to implement a sliding window, which allows users to group data points together, allowing for aggregation and other multi-value operations.

The following code is an example usage of the Stream Processing Engine with Minecraft being an input and output point for the engine. The code below reads in Minecraft chunk data, depending on the player's current region (16x16 chunks). Then, it forwards chunk data to an operator that finds the 'best' chunk for the player to be in, based on the number of diamonds found utilizing an aggregator operator. Finally, the chunk location and diamond locations are forwarded to another operator, which send the data back via TCP connection. My contribution on this demo of the engine also includes work in Python and Java, parsing the world data of Minecraft to send to the engine and receiving the coordinates in game.

Github Repo


// Copyright 2019 [BVU CMSC491 class]
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#include <cmath>
#include "SPE.h"
#include "TCPListener.h"
#include "MinecraftRegionLoader.h"


TCPListener listener;

struct aggData {
  float chunkVal;
  pos chunkID;
  std::vector<pos>* oreLocations;
} typedef aggData;


float calcDistance(pos playerPos, pos chunkPos) {
  float x = pow(playerPos.x - chunkPos.x, 2);
  float y = pow(playerPos.y - chunkPos.y, 2);
  float z = pow(playerPos.z - chunkPos.z, 2);
  float dist = sqrt(x+y+z);
  return dist;
}


pos getBlockPos(int i, pos globalChunkPos) {
  //i = y*16*16 + z*16 + x
  pos p;
  int relx, rely, relz, diff;

  rely = i/256;
  diff = i-(rely*256);
  relx = diff % 16;
  relz = (diff - relx)/16;

  p.y = rely + globalChunkPos.y;
  p.z = relz + globalChunkPos.z;
  p.x = relx + globalChunkPos.x;

  return p;
}


class ChunkSelect : public Operator {
  public:
    ChunkSelect(int r, int s) : Operator(r, s) {}
    void processData(Data data) {
      std::cout << "chunk select recv one data" << std::endl;
      emit(Data(&data, sizeof(aggData)));
      std::cout << "chunk select emit data" << std::endl;
    }

    void processData() {
      std::cout << "chunk select recv data" << std::endl;
      aggData bestAgg;
      bestAgg.chunkVal = -1;
      for (Data d : window) {
        if(bestAgg.chunkVal == -1 || bestAgg.chunkVal < (*(aggData*)d.value).chunkVal ) {
          if(bestAgg.chunkVal != -1) {
            delete bestAgg.oreLocations;
          }
          bestAgg = *(aggData*)(d.value);
        }
        else {
          delete (*(aggData*)d.value).oreLocations;
        }
      }

      std::cout << "chunk select emit data" << std::endl;
      emit(Data(&bestAgg, sizeof(aggData)));
    }
};


class ChunkProcessor : public Operator {
  public:
    void processData(Data data) {
      //std::cout << "chunk procesessor recv data" << std::endl;
      ChunkData &chunk = *(ChunkData*)data.value;

      // Handle an empty chunk
      if (chunk.empty == true) {
        aggData dataToPass;
        dataToPass.oreLocations = new std::vector<pos>
        dataToPass.chunkVal = 0;
         
        //std::cout << "[empty] chunk procesessor emit data" << std::endl;
        emit(Data(&dataToPass, sizeof(aggData)));
        return;
      }

      // Handle an non-empty chunk
      float count = 0;
      aggData dataToPass;
      std::vector<pos>* oreLocations = new std::vector<pos>
      dataToPass.chunkID = chunk.globalChunkPos;

      dataToPass.oreLocations = oreLocations;
      for(int i = 0; i<65536 ; i++) {
        if(chunk.chunk[i] == chunk.oreID){
          count++;
          oreLocations->push_back(getBlockPos(i, chunk.globalChunkPos));
        }
      }

      dataToPass.chunkVal = count/calcDistance(chunk.playerPos, chunk.globalChunkPos);

      //std::cout << "[non-empty] chunk procesessor emit data with #ores:" << oreLocations->size() << std::endl;
      emit(Data(&dataToPass, sizeof(aggData)));
    }
};


class Generator : public InputSource {
  void generateData() {

    std::cout << "Waiting for data from mod..." << std::endl;

    std::string strPos;
    while ( (strPos = listener.GetLine()) != "") {
      std::istringstream is(strPos);
      pos playerPos;
      is >> playerPos.x >> playerPos.y >> playerPos.z;
      std::cout << "Player pos at: " <<
        playerPos.x << " " <<
        playerPos.y << " " <<
        playerPos.z << std::endl;

      //pos playerPos = {0,0,0};
      MinecraftRegionLoader loader(playerPos);
      std::vector<ChunkData*> chunks = loader.extractChunkData();

      for(int i=0; i < chunks.size(); i++) {
        chunks[i]->oreID = 26;
        emit(Data(chunks[i], sizeof(ChunkData)));
      }

      std::cout << "Produced " << chunks.size() << "chunks. Waiting for more data from mod..." << std::endl;
    }

    std::cout << "Minecraft Mod Disconnected" << std::endl;
  }
};


class PrintOp : public Operator{
  public:
    void processData(Data data){

      //std::cout << "chunk print recv data" << std::endl;
      aggData bestChunk = *(aggData*)data.value;
      std::vector<pos> ores = *(bestChunk.oreLocations);
      //std::cout << "ores size: " << ores.size() << std::endl;
      for(int i=0; i<ores.size(); i++){
       std::cout <<"Chunk: " << bestChunk.chunkID.x << " " << bestChunk.chunkID.z << " " << bestChunk.chunkID.y << std::endl;
       std::cout <<"Desired Ore at pos: " << std::endl;
       std::cout << "x: " << ores[i].x << std::endl;
       std::cout << "z: " << ores[i].z << std::endl;
       std::cout << "y: " << ores[i].y << std::endl;
      }

      if (ores.empty() == false) {
        std::ostringstream os;
        os << "Get ore at " << ores[0].x << " " << ores[0].y << " " << ores[0].z;
        string msg = os.str();
        uint8_t msgSz = msg.size();

        listener.SendData((char*)&msgSz, sizeof(msgSz));
        listener.SendData(msg.c_str(), msgSz);
      }

      delete bestChunk.oreLocations;
    }
};

int main(int argc, char** argv) {
  std::cout << "SPE Starting up." << std::endl;

  int port = 12345;
  listener.Bind(port);

  std::cout << "Waiting for connection..." << std::endl;
  if (listener.WaitForConnection() <= 0) {
    std::cout << "Error - failed to connect" << std::endl;
    return 0;
  }

  Generator inputSource;
  ChunkProcessor op1;
  ChunkSelect op2(1024, 1024);
  PrintOp op3;

  StreamProcessingEngine spe;

  spe.addInputSource(&inputSource, {&op1});
  spe.connectOperators(&op1, {&op2});
  spe.connectOperators(&op2, {&op3});

  spe.run();

  std::cout << "SPE Finished." << std::endl;
  return 0;
}
				

Item Trading over an Online Marketplace

I.T.O.M. - Full Stack

Item Trading over an Online Marketplace is a full stack application that allows the trading of items between players regardless of the game the users play. This allows users to trade items between games such as Minecraft and Runescape - trading items from one game for items from another.

I.T.O.M. was my undergraduate capstone project that I worked on with one other student over the course of a semester. Together, we created a full stack application with custom protocols including a database with serialized game data for in game items. The original pitch included two games: Minecraft and Runescape - due to unforseen scoping, this was reduced to only Minecraft. However, the system theoretically supports multiple games, with Runescape being partially implemented game-side utilizing an automated player that facilitates player trades.

Utilizing React for the frontend, Spring for the backend, and Postgres as our DBMS, the application was developed in HTML, Javascript, and Java with SQL calls for database interactions. Below are some sample images of the application in use and its architecture.

Frontend Repo
Backend Repo



Memory Manager

More description coming soon, example code below for HeapManager.cpp. Supports byte alignment.



#include <Windows.h>

#include "HeapManager.h"
#include <cassert>
#include <stdio.h>
#include <memoryapi.h>

bool IsPowerOfTwo(unsigned int x)
{
	return (x & (x - 1)) == 0;
}

inline unsigned int AlignUp(unsigned int i_value, unsigned int i_align)
{
	assert(i_align);
	assert(IsPowerOfTwo(i_align));
	return (i_value + (i_align - 1)) & ~(i_align - 1);
}

inline unsigned int AlignDown(unsigned int i_value, unsigned int i_align)
{
	assert(i_align);
	assert(IsPowerOfTwo(i_align));
	return i_value & ~(i_align - 1);
}



HeapManager::HeapManager(void* i_memStart, const size_t i_maxSize, const unsigned int i_numDescriptors) {
	unsigned int overhead = sizeof(HeapManager) + (sizeof(Descriptor) * i_numDescriptors);

	m_numDescriptors = i_numDescriptors;
	m_memStart = i_memStart;

	// Starting at beginning of total block (i_memStart), write descriptors. They are accounted for in overhead.
	m_unusedBlocks = reinterpret_cast<Descriptor*>(reinterpret_cast<char*>(i_memStart) + sizeof(HeapManager));
	Descriptor* d = m_unusedBlocks;
	for (unsigned int i = 0; i < i_numDescriptors; i++) {
		d->startAddr = nullptr;
		d->size = 0;
		d->next = d + 1;
		d++;
	}
	(d - 1)->next = nullptr; // Set the last item's next to null

	m_maxSize = i_maxSize - overhead;
	m_outstandingAllocList = nullptr;
	m_freeList = getUnusedBlock();
	m_freeList->startAddr = reinterpret_cast<char*>(i_memStart) + overhead;
	m_freeList->size = m_maxSize;
	m_freeList->next = nullptr;
}

HeapManager* HeapManager::CreateHeapManager(void* i_memStart, const size_t i_maxSize, const unsigned int i_numDescriptors) {
	assert(i_memStart != nullptr && i_maxSize > 0 && i_numDescriptors > 0);

	HeapManager* manager = reinterpret_cast<HeapManager*>(i_memStart);

	return new (manager) HeapManager(i_memStart, i_maxSize, i_numDescriptors);
}

size_t HeapManager::GetLargestFreeBlock() {
	assert(m_freeList != nullptr);
	Descriptor* d = m_freeList;

	size_t maxSize = d->size;
	while (d != nullptr) {
		maxSize = (d->size > maxSize) ? d->size : maxSize;
		d = d->next;
	}

	return maxSize;
}

void* HeapManager::alloc(size_t i_size) {
	Descriptor* d = getFirstFreeBlock(i_size, 1);

	// There is either not enough space or not enough blocks.
	if (d == nullptr)
		return nullptr;

	// Add to outstanding allocations
	d->next = m_outstandingAllocList;
	m_outstandingAllocList = d;

	return d->startAddr;
}


void* HeapManager::alloc(size_t i_size, int i_alignment) {
	Descriptor* d = getFirstFreeBlock(i_size, i_alignment);

	// There is either not enough space or not enough blocks.
	if (d == nullptr)
		return nullptr;

	// Add to outstanding allocations
	d->next = m_outstandingAllocList;
	m_outstandingAllocList = d;

	return d->startAddr;
}


bool HeapManager::free(void* i_addr) {
	// Nothing allocated.
	if (m_outstandingAllocList == nullptr)
		return false;

	// Check if it is first
	if (m_outstandingAllocList->startAddr == i_addr) {
		Descriptor* desc = m_outstandingAllocList;
		// remove from outstanding allocations
		m_outstandingAllocList = desc->next;

		// add into free list
		InsertToFreeList(desc);
		return true;
	}

	// Get the related descriptor
	Descriptor* prev = m_outstandingAllocList;
	Descriptor* cur = prev->next;
	while (cur != nullptr) {
		if (cur->startAddr == i_addr) {
			// Remove from outstanding allocations
			prev->next = cur->next;

			// Add into free list
			InsertToFreeList(cur);

			return true;
		}

		prev = cur;
		cur = prev->next;
	}
	
	// Not allocated address
	return false;
}

bool HeapManager::Contains(void* i_addr) {
	if (m_outstandingAllocList == nullptr)
		return false;

	Descriptor* d = m_outstandingAllocList;

	while (d != nullptr) {
		if (i_addr >= d->startAddr && i_addr <= reinterpret_cast<char*>(d->startAddr) + d->size) {
			return true;
		}

		d = d->next;
	}

	return false;
}

bool HeapManager::IsAllocated(void* i_addr) {
	if (m_outstandingAllocList == nullptr)
		return false;

	Descriptor* d = m_outstandingAllocList;

	while (d != nullptr) {
		if (d->startAddr == i_addr) {
			return true;
		}

		d = d->next;
	}

	return false;
}

/*
	Garbage collect - coalesce adjacent blocks
*/
void HeapManager::Collect() {
	// There is nothing to combine if there is none or only one block.
	if (m_freeList == nullptr)
		return;
	if (m_freeList->next == nullptr)
		return;

	bool madeChange = true;
	while (madeChange) {
		madeChange = false;

		Descriptor* prev = m_freeList;
		Descriptor* cur = prev->next;

		while (cur != nullptr) {
			char* endPoint = reinterpret_cast<char*>(prev->startAddr) + prev->size;
			char* curStartPoint = reinterpret_cast<char*>(cur->startAddr);

			// If they are one off, then they're adjacent
			if (endPoint == curStartPoint) {
				// nom nom nom
				prev->size += cur->size;
				cur->size = 0;
				prev->next = cur->next;

				// Add cur to unused blocks
				cur->next = m_unusedBlocks;
				m_unusedBlocks = cur;

				// Move cur because now it's in unused blocks
				cur = prev;
				madeChange = true;
			}

			// Move to the next set
			prev = cur;
			cur = cur->next;
		}
	}
}


/*
	Gets the first free block that fits the given size. Removes the descriptor from free list and adds
	a new one for the remaining size. 
*/
Descriptor* HeapManager::getFirstFreeBlock(size_t i_size, int i_alignment) {
	// There is no free blocks
	if (m_freeList == nullptr)
		return nullptr;

	// If the first is large enough, allocate it (account for potentially required alignment)
	if (m_freeList->size > i_size + i_alignment) {
		Descriptor* desc = m_freeList;

		// Get an unused block, put it at the start of freeList
		m_freeList = getUnusedBlock();
		
		//if getUnusedBlock returns nullptr, revert
		if (m_freeList == nullptr) {
			m_freeList = desc;
			return nullptr;
		}

		m_freeList->startAddr = desc->startAddr;
		m_freeList->next = desc->next;

		// Move from back to front to allocate space
		char* unalignedAddr = reinterpret_cast<char*>(desc->startAddr) + desc->size - i_size;
		unsigned int movedAddr = AlignDown(reinterpret_cast<unsigned int>(unalignedAddr), i_alignment);
		desc->startAddr = reinterpret_cast<void*>(movedAddr);

		// Account for the newly aligned sizes
		m_freeList->size = reinterpret_cast<char*>(desc->startAddr) - reinterpret_cast<char*>(m_freeList->startAddr);
		desc->size = i_size + reinterpret_cast<unsigned int>(unalignedAddr) - movedAddr;

		return desc;
	}

	Descriptor* prev = m_freeList;
	Descriptor* cur = prev->next;
	while (cur != nullptr) {
		if (cur->size > i_size + i_alignment) {
			// Set up a replacement for the remaining size, set up pointers accordingly
			Descriptor* replacement = getUnusedBlock();

			// If getUnusedBlock returns nullptr, bail
			if (replacement == nullptr)
				return nullptr;

			replacement->startAddr = cur->startAddr;
			replacement->next = cur->next;

			// Move from back to front to allocate space
			char* unalignedAddr = reinterpret_cast<char*>(cur->startAddr) + cur->size - i_size;
			unsigned int movedAddr = AlignDown(reinterpret_cast<unsigned int>(unalignedAddr), i_alignment);
			cur->startAddr = reinterpret_cast<void*>(movedAddr);

			prev->next = replacement;

			// Update size and return
			cur->size = i_size + reinterpret_cast<unsigned int>(unalignedAddr) - movedAddr;
			replacement->size = reinterpret_cast<char*>(cur->startAddr) - reinterpret_cast<char*>(replacement->startAddr);
			return cur;
		}

		prev = cur;
		cur = prev->next;
	}

	// None of size
	return nullptr;
}

Descriptor* HeapManager::getUnusedBlock() {
	if (m_unusedBlocks == nullptr)
		return nullptr;

	Descriptor* o_desc = m_unusedBlocks;
	m_unusedBlocks = o_desc->next;

	return o_desc;
}


void HeapManager::ShowOutstandingAllocations() {
	Descriptor* d = m_outstandingAllocList;

	printf("Allocated List:\n");
	while (d != nullptr) {
		printf("\t selfAddr: %p \t startAddr: %p \t size:%d \t next: %p\n", d, d->startAddr, d->size, d->next);
		d = d->next;
	}
}

void HeapManager::ShowFreeBlocks() {
	Descriptor* d = m_freeList;

	printf("Free List:\n");
	while (d != nullptr) {
 		printf("\t selfAddr: %p \t startAddr: %p \t size:%d \t next: %p\n", d, d->startAddr, d->size, d->next);
		d = d->next;
	}
}

void HeapManager::InsertToFreeList(Descriptor* i_desc) {
	// Check if it is the first item
	if (m_freeList == nullptr || m_freeList->startAddr > i_desc->startAddr) {
		// push onto front
		i_desc->next = m_freeList;
		m_freeList = i_desc;

		return;
	}
	
	// Otherwise, iterate list and place in sorted order
	Descriptor* prev = m_freeList;
	Descriptor* cur = prev->next;
	while (cur != nullptr) {
		if (prev->startAddr < i_desc->startAddr && cur->startAddr > i_desc->startAddr) {
			// Place in between
			prev->next = i_desc;
			i_desc->next = cur;

			return;
		}

		prev = cur;
		cur = prev->next;
	}
	
	// Finally, if it was never placed, put it at the end
	prev->next = i_desc;
	i_desc->next = nullptr;
}


void HeapManager::Destroy() {
	VirtualFree(m_memStart, 0, MEM_RELEASE);
}