Dungeons and Portals | Mount Sinai Hospital
Description
The Dungeons & Portals Game is a platformer game developed in vanilla JavaScript within a Flask application.
Link: https://dungeons-and-portals.onrender.com
Code: https://github.com/emberlzhang/task_dungeon-portals-game
Purpose
The Dungeons & Portals Game was designed for the Center for Computational Psychiatry at Mount Sinai Hospital to differentiate between clinical populations with behavioral compulsions and healthy control (HC) populations.
Overview
Each round of the game, the player chooses a dungeon to enter, then tries to escape the dungeon using the fewest number of portal teleportations possible.
Gameplay Mechanics
-
The game begins on the Home Screen, where the player chooses to enter one of two dungeons.
-
Unknown to the player, the two dungeons operate on the following underlying logic:
- Option A: High reward, high cost - always results in a net negative point balance.
- Option B: Moderate reward, no cost - always results in a net positive point balance.
-
Upon entering a dungeon, the player earns a certain number of points.
-
Each dungeon contains portals, with the goal of finding a way to the “escape” door.
- The player navigates through these portals, which teleport them to different sections of the dungeon, until they reach the escape door.
- Depending on whether the dungeon represents Option A or B, the portals may cause the player to lose points.
-
Once the player escapes the dungeon, they return to the Home Screen to make another choice.
Features
Teleportation Logic
The player’s ability to “teleport” through portals in the game was enabled using algorithms run at the beginning of the game session.
- The portal_generator algorithm randomly generates portal-to-portal connections in the form of integer key-value pairs.
def portal_generator(self):
if self.n_doors % 2 != 0:
raise Exception("Number of doors needs to be even.")
# this is because every door needs a connecting door
doors = list(range(self.n_doors)) # make array list of size n_doors
doors = [i + 1 for i in doors] # add 1 to each array item to account for index zero
portal_connections = {}
# for connection in range(1, n_connections):
while len(doors) > 0:
random.shuffle(doors)
point_a = doors[0]
doors.remove(point_a)
point_b = doors[0]
doors.remove(point_b)
portal_connections[point_a] = point_b
portal_connections[point_b] = point_a
floors_n_doors = {}
n_doorsperfloor = math.ceil(self.n_doors / self.n_floors) # round up to nearest integer
# assign doors to each floor
doors_list = list(range(1, self.n_doors+1))
for floor in range(1, self.n_floors+1): # 1 thru 4 floors floorsndoors[4] = range 3*3+1, 10
doors = doors_list[:n_doorsperfloor]
floors_n_doors[floor] = doors # assign doors to floor
doors_list = doors_list[n_doorsperfloor:] # remove doors of that floor from doors_list
assignments = [portal_connections, floors_n_doors]
return assignments
- The portal_solver algorithm determines the shortest path (number of teleportations) to “solve” the portal map.
def portal_generator(self):
if self.n_doors % 2 != 0:
raise Exception("Number of doors needs to be even.")
# this is because every door needs a connecting door
doors = list(range(self.n_doors)) # make array list of size n_doors
doors = [i + 1 for i in doors] # add 1 to each array item to account for index zero
portal_connections = {}
# for connection in range(1, n_connections):
while len(doors) > 0:
random.shuffle(doors)
point_a = doors[0]
doors.remove(point_a)
point_b = doors[0]
doors.remove(point_b)
portal_connections[point_a] = point_b
portal_connections[point_b] = point_a
floors_n_doors = {}
n_doorsperfloor = math.ceil(self.n_doors / self.n_floors) # round up to nearest integer
# assign doors to each floor
doors_list = list(range(1, self.n_doors+1))
for floor in range(1, self.n_floors+1): # 1 thru 4 floors floorsndoors[4] = range 3*3+1, 10
doors = doors_list[:n_doorsperfloor]
floors_n_doors[floor] = doors # assign doors to floor
doors_list = doors_list[n_doorsperfloor:] # remove doors of that floor from doors_list
assignments = [portal_connections, floors_n_doors]
return assignments
- The portal_picker algorithm keeps running portal_maker and portal_solver until finding a set of portals that met the criteria set by the game in terms of portal map solving difficulty. (For example, in one of our main game setups, we set the difficulty of the game dungeons to take a minimum of 3 steps-to-solve.)
def portal_picker(self):
steps = 0
reps = 0
while steps != self.steps_to_solve:
reps += 1
assignments = self.portal_generator() # generate new set of portal connections
portal_connections = assignments[0]
floors_n_doors = assignments[1]
steps = self.portal_solver(portal_connections, floors_n_doors) # solve for this set of portal connections
if steps == self.steps_to_solve: # if it matches, return this set of portal connections
break
if reps >= 100:
raise Exception("More than 100 failed attempts to find portal connections. Try different portal parameters.")
# print("Portal set found!")
# print("Steps to solve: " + str(steps))
self.portal_connections = portal_connections
return portal_connections
These algorithms return a dictionary mapping two unique sets of portal-to-portal connections for the creation of two dungeons for each user / game session.
These algorithms also ensure a consistent level of game difficulty across different user sessions.
Real-Time Animations
An animation loop (60 ms frame rate) is run to allow real-time animations. The Player object is made to appear as a living and moving character through a set of sprite images set to repeatedly cycle at a certain refresh rate based on the game frame rate.
Collision Management
The use of “collision block” objects allows for the player to navigate the entire platform game map without colliding into any visible walls or the ground.
The game’s Map object would represent information about each dungeon in the game and denote the borders through a set of collision blocks.
At each frame refresh of the game, functions like checkHorizontalCollisions (shown here) and checkVerticalCollisions would run to ensure the player’s position was not colliding with the pre-set collision blocks made by the Map object (as shown above).
checkHorizontalCollisions(collisionBlocks) {
for (let i = 0; i < collisionBlocks.length; i++) {
const collisionBlock = collisionBlocks[i]
// if player collision exists
if (
this.position.x <= collisionBlock.position.x + collisionBlock.width && // left side of player hits collision block
this.position.x + this.displayWidth >= collisionBlock.position.x && // right side of player hits collision block
this.position.y + this.displayHeight >= collisionBlock.position.y && // bottom of player hits collision block
this.position.y <= collisionBlock.position.y + collisionBlock.height // top of player hits collision block
) {
// player colliding while going to the left
if (this.velocity.x < 0) {
// reset player position to avoid collision
this.position.x = collisionBlock.position.x + collisionBlock.width + 0.01
break
}
// player colliding while going to the right
if (this.velocity.x > 0) {
// reset player position to avoid collision
this.position.x = collisionBlock.position.x - this.displayWidth - 0.01
break
}
}
}
}
Significance to End User
The game tracks the player’s ability to accurately model cost-benefit in complex-deterministic environments, and to make rational choices to maximize game rewards.
Deviations from expected behavior may indicate an inability to correctly model cost-benefit of complex decisions. Correlation between irrational behavioral choices and behavioral compulsions may suggest that the inability to correctly model complex environments underlies behavioral compulsions in clinical populations.
Player behavior and preferences are mapped to Bayesian inference models to discern group-level differences between HC populations and clinical behavioral compulsion groups.