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)
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
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
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.
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.
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
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;
}
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
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);
}