Crystal Cave

A rogue-like game designed with playing cards recreated in JavaScript

Play:

[ Health♡: ] [ Gems♢: ] [ Bombs⚪: ]
[ Crystals: ]

/*
    Planning Lists:
    --------------
    
    Modules:
    setBoard()
    drawBoard()
    displayGameInfo()
    movePlayer()
    updateView()
    checkAdjacent()
    pickUpItem()
    placeBomb()
    checkWin()
    checkLose()
    
    Input Variables:
    direction
    
    Output Variables:
    board[[][][][][][]]
    view[[][][][][][]]
    playerHealth
    hearts
    gems
    bombs
    aceOfHearts
    aceOfDiams
    aceOfClubs
    aceOfSpades
    
    
    Process:
    // Constants
    MAX_HEALTH = 10;
    
    // Global Variables
    board[[0,0,0,0,0,0][][][][][]]
    view[[][][][][][]]
    player{x:0,y:0,health:10}
    hearts
    gems
    bombs
    aceOfHearts
    aceOfDiams
    aceOfClubs
    aceOfSpades
    
    function setBoard
        initialize an array
        add 4 Ks
        add 4 Qs
        add 4 Js
        add 1 P
        add 12 walls
        add 6 hearts
        add 6 gems
        shuffle the array
        find and store the players location (P)
        set players health to MAX_HEALTH
        set gems to 0
        set hearts to 0
        set bombs to 0
        set all aces to false
        reset the view array
        drawBoard()
        
    function placeBomb
        check each adjacent square to the player for a wall
        if there is a wall, remove it.
        
    function movePlayer
        move player in the direction according to input
        updateView()
        pickUpItem()
        checkAdjacent()
        drawBoard()
        displayGameInfo()
        checkWin()
        checkLose()
        
    
    HTML Entities:
    ♠
    ੨
    ♣
    ੫
    ♥
    ੥
    ♦
    ੦
    º
    ¤
    # ?
*/
// Global Constants
MAX_HEALTH = 10;

// Global Variables
var board = [[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0],[0,0,0,0,0,0]];
var view;
var player = {x:0,y:0,health:MAX_HEALTH};
var narrative = "";
var drops = [];
var gems = 0;
var bombs = 0;
var aceOfHearts = false;
var aceOfDiams = false;
var aceOfClubs = false;
var aceOfSpades = false;


/*  PURPOSE:    to return a psuedorandom integer between the min and max parameters
    PARAMETERS: min         - minimum possible value to be returned
                max         - maximum possible value to be returned
*///RETURNS:    newNumber   - a psuedorandom integer that is either between or is one of the min or max paramters
function randomRange(min,max)
{
    var newNumber = Math.floor((Math.random() * (max - min)) + min);
    return newNumber;
}

//  PURPOSE:    to output the gameboard as a table of html entities and characters to the webpage
function drawBoard()
{
    // Declare Variables
    var outputBoard = document.getElementById("board");
    var outputHTML;
    
    // Initialize
    outputHTML = "";
    
    // Create table cells from the board array
    for(var i = 0; i < 6; i++) // x axis
    {
        outputHTML += "<tr>";
        for(var j = 0; j < 6; j++) // y axis
        {
            if (view[i][j] === false && board[i][j] !== "P")
            {
                outputHTML += "<td class=\"text-muted\">?</td>";
            }
            else
            {
                switch (board[i][j])
                {
                    case "K":
                        outputHTML += "<td class=\"text-danger\">K</td>";
                        break;
                    case "Q":
                        outputHTML += "<td class=\"text-danger\">Q</td>";
                        break;
                    case "J":
                        outputHTML += "<td class=\"text-danger\">J</td>";
                        break;
                    case "S":
                        outputHTML += "<td>♠</td>";
                        break;
                    case "C":
                        outputHTML += "<td>♣</td>";
                        break;
                    case "D":
                        outputHTML += "<td>♦</td>";
                        break;
                    case "H":
                        outputHTML += "<td>♥</td>";
                        break;
                    case "P":
                        if (player.health > 0)
                        {
                            outputHTML += "<td class=\"text-primary\"><b>@</b></td>";
                        }
                        else
                        {
                            outputHTML += "<td class=\"text-danger\"><b>@</b></td>";
                        }
                        break;
                    case "W":
                        outputHTML += "<td><b>#</b></td>";
                        break;
                    case "h":
                        outputHTML += "<td>♡</td>";
                        break;
                    case "dh":
                        outputHTML += "<td class=\"text-danger\">♡</td>";
                        break;
                    case "g":
                        outputHTML += "<td>♢</td>";
                        break;
                    case "dg":
                        outputHTML += "<td class=\"text-danger\">♢</td>";
                        break;
                    case "B":
                        outputHTML += "<td>⚪</td>";
                        break;
                    case "dB":
                        outputHTML += "<td class=\"text-danger\">⚪</td>";
                        break;
                    case "":
                        outputHTML += "<td></td>";
                        break;
                    default:
                        outputHTML += "<td>Error: " + board[i][j] + "</td>";
                }   
            }
        }
        outputHTML += "</tr>";
    }
    
    // Output to the page
    outputBoard.innerHTML = outputHTML;
}

//  PURPOSE:    to output game information health, gems, bombs, and crystals collected to the webpage
function displayGameInfo()
{
    // Declare Variables
    var aces = "";
    
    // Count Aces
    if (aceOfClubs) { aces += "♣"; }
    if (aceOfDiams) { aces += "♦"; }
    if (aceOfHearts) { aces += "♥"; }
    if (aceOfSpades) { aces += "♠"; }
    
    // Display Player Info On The Page
    document.getElementById("health").innerHTML = player.health;
    document.getElementById("gems").innerHTML = gems;
    document.getElementById("bombs").innerHTML = bombs;
    document.getElementById("jewels").innerHTML = aces;
    
}

//  PURPOSE:    to update the array mapping which cells in the table the player is able to view
function updateView()
{
    // North
    if ((player.y-1) >= 0)
    {
        view[player.x][player.y-1] = true;
        // North East
        if ((player.x+1) <= 5)
        {
            view[player.x+1][player.y-1] = true;
        }
    }
    // East
    if ((player.x+1) <= 5)
    {
        view[player.x+1][player.y] = true;
        // South East
        if ((player.y+1) <= 5)
        {
            view[player.x+1][player.y+1] = true;
        }
    }
    // South
    if ((player.y+1) <= 5)
    {
        view[player.x][player.y+1] = true;
        // South West
        if ((player.x-1) >= 0)
        {
            view[player.x-1][player.y+1] = true;
        }
    }
    // West
    if ((player.x-1) >= 0)
    {
        view[player.x-1][player.y] = true;
        // North West
        if ((player.y-1) >= 0)
        {
            view[player.x-1][player.y-1] = true;
        }
    }  
}

//  PURPOSE:    to check adjacent tiles for anything the player interacts with inherently by being next to. these currently consist only of enemy characters
function checkAdjacent()
{
    // Declare Variables
    var cellsX = [];
    var cellsY = [];
    var cell;
    
    // Set Coordinates
    cellsX[0] = player.x-1;    // North
    cellsY[0] = player.y;
    cellsX[1] = player.x;      // East
    cellsY[1] = player.y+1;
    cellsX[2] = player.x+1;    // South
    cellsY[2] = player.y;
    cellsX[3] = player.x;      // West
    cellsY[3] = player.y-1;
    
    // Check for enemies
    for(var i = 0; i < 4; i++)
    {
        // If the target tile to view is on the board
        if (cellsX[i] >= 0 && cellsX[i] <= 5 && cellsY[i] >= 0 && cellsY[i] <= 5)
        {
            // Get its contents
            cell = board[cellsX[i]][cellsY[i]];
    
            // Check if it is an enemy. If it is, take the appropriate amount of health from the player before removing it
            switch(cell)
            {
                case "K":
                    player.health -= 3;
                    board[cellsX[i]][cellsY[i]] = drops.pop();
                    narrative += " You find your self face to face with a giant King Crab. Your sword pierces the soft underside of it's shell as it latches onto your nose with it's claw. It finally breaths its last leaving you down 3 health points.";
                    narrative += "<span class=\"text-danger\"><b><br />-3 HP<br /></b></span>"
                    break;
                case "Q":
                    player.health -= 2;
                    board[cellsX[i]][cellsY[i]] = drops.pop();
                    narrative += " In the distance you spot a large yellow structure. As you approach you find it to be an enormous bee hive. Your realization comes too late as you are surrounded by the minions of the Queen Bee. You light them ablaze with fire magic, but not before recieving a few painful stings and a loss of 2 health points.";
                    narrative += "<span class=\"text-danger\"><b><br />-2 HP<br /></b></span>"
                    break;
                case "J":
                    player.health -= 1;
                    board[cellsX[i]][cellsY[i]] = drops.pop();
                    narrative += " A large pot flies through the air landing firmly against your forehead. You chase down the culprit and teach them what for. Unfortunatly this doesn't replenish the health point you've lost.";
                    narrative += "<span class=\"text-danger\"><b><br />-1 HP<br /></b></span>"
                    break;
            }
            
            // Set players health text to red if less than 4
            if (player.health < 4)
            {
                document.getElementById("health").className = " text-danger";
            }
        }
    }
}

//  PURPOSE:    to check the table cell the player has moved to for items and if found add the item to the appropriate counter variable
function pickUpItem()
{
    // Declare Variables
    var item = board[player.x][player.y];
    
    // Add item to appropriate counter
    switch (item)
    {
        case "S":
            aceOfSpades = true;
            narrative += "<span class=\"text-success\"><b> You find the Obsidian Spade upon an ornate pedistol. Carefully, you remove it from its resting place.</b></span><br />";
            break;
        case "C":
            aceOfClubs = true;
            narrative += "<span class=\"text-success\"><b> The Emerald Clover catches your eye as you nearly walk over it. You proudly claim it for your own.</b></span><br />";
            break;
        case "D":
            aceOfDiams = true;
            narrative += "<span class=\"text-success\"><b> Set in the wall you find the largest Diamond you have ever seen. You quickly pocket it and move on.</b></span><br />";
            break;
        case "H":
            aceOfHearts = true;
            narrative += "<span class=\"text-success\"><b> At the bottom of a pool of water, nearly frozen over, you spot the Ruby Heart. Talk about cold... You gently dry it off and place it in your satchel.</b></span><br />";
            break;
        case "B":
        case "dB":
            bombs++;
            document.getElementById("bombs").className = " text-success";
            narrative += " On the ground near the defeated menace is a small bomb. This could be usefull.<br />";
            break;
        case "h":
        case "dh":
            // Increase health 1 per heart pickup
            player.health++;
            if (player.health > 3)
            {
                document.getElementById("health").className = " text-success";
            }
            narrative += " You feel your strength returning as you regain 1 health point.";
            narrative += "<span class=\"text-success\"><b><br />+1 HP<br /></b></span>"
            break;
        case "g":
        case "dg":
            gems++;
            narrative += " You find a small gemstone on the ground. This could be quite valuable.<br />";
            break;
    }
}

//  PURPOSE:    to check if the player has collected all 4 crystals and congratulate them if they have
function checkWin()
{
    if (aceOfClubs && aceOfDiams && aceOfHearts && aceOfSpades)
    {
        // Congratulate the player on their victory
        narrative += "<span class=\"text-success\"><br /><b>You've done it. The crystals are yours! You are now a legend.</b><br /></span>";
    }
}

//  PURPOSE:    to check if the player has run out of health and died and inform them if they have
function checkLose()
{
    if (player.health <= 0)
    {
        player.health = 0;
        displayGameInfo();
        // Tell the player they lost
        narrative += "<span class=\"text-danger\"><br /><b>Your ghost lingers, haunting the confines of the crystal cave.</b><br /></span>";
    }
}

//  PURPOSE:    to output collected narration text for the given turn to the webpage
function narrate()
{
    document.getElementById("textOutput").innerHTML = "<b>" + narrative + "</b>";
    narrative = "";
}

//  PURPOSE:    to reset the viewmap so the player can't see the whole map when they start
function setView()
{
    view = [[false,false,false,false,false,false],[false,false,false,false,false,false],
            [false,false,false,false,false,false],[false,false,false,false,false,false],
            [false,false,false,false,false,false],[false,false,false,false,false,false]];
    narrative += " A fog fills the space, settling as you move through it.";
}

//  PURPOSE:    to generate the starting conditions for the game
function setBoard()
{
    // Declare Variables
    var deck = ["K","K","K","K",
                "Q","Q","Q","Q",
                "J","J","J","J",
                "S","C","D","H",
                "P",
                "W","W","W","W",
                "W","W","W","W","W",
                "h","h","h","h","h",
                "g","g","g","g","g"];
    var tmp;
    var target;
    var deckLoopCounter;
    
    // Shuffle the Array
    for(var i = 0; i < 36; i++)
    {
        tmp = deck[i];
        target = randomRange(i,35);
        deck[i] = deck[target];
        deck[target] = tmp;
    }
    
    // Layout the array onto the board
    deckLoopCounter = 0;
    for(var i = 0; i < 6; i++) // x axis
    {
        for(var j = 0; j < 6; j++) // y axis
        {
            board[i][j] = deck[deckLoopCounter];
            
            //find and store the players location (P)
            if (board[i][j] === "P")
            {
                player.x = i;
                player.y = j;
            }
            deckLoopCounter++;
        }
    }
    
    // Set up item drop deck
    drops = ["dB","dB","dB","dg",
             "dg","dg","dh","dh",
             "dh","dh","dh","dh",];
    
    // Shuffle item drop deck
    for(var i = 0; i < 12; i++)
    {
        tmp = drops[i];
        target = randomRange(i,11);
        drops[i] = drops[target];
        drops[target] = tmp;
    }
    
    // Set game counters to default values
    player.health = MAX_HEALTH;
    gems = 0;
    //hearts = 0;
    bombs = 0;
    aceOfHearts = false;
    aceOfDiams = false;
    aceOfClubs = false;
    aceOfSpades = false;
    
    // Reset text display classes
    document.getElementById("health").className = "";
    document.getElementById("bombs").className = "";
    
    // Reset the view array
    setView();
    updateView();
    
    // Begin the adventure narrative
    narrative += " You find yourself, once again, in a cave. It's been about a month since your arrival. <span class=\"text-success\">Your mission: To collect the four great crystaline gem stones.</span> Crystals growing from the floor shimmer brightly as if to show you the way.<br /><br />";    

    // Draw the Board
    checkAdjacent();
    drawBoard();
    displayGameInfo();
    narrate();
}

//  PURPOSE:    called directly from the webpage, to use a bomb item and remove walls around the player character
function placeBomb ()
{
    if (bombs > 0)
    {
        // Declare Variables
        var cellsX = [];
        var cellsY = [];
        
        // Set Coordinates to check
        cellsX[0] = player.x-1;    // North
        cellsY[0] = player.y;
        cellsX[1] = player.x;      // East
        cellsY[1] = player.y+1;
        cellsX[2] = player.x+1;    // South
        cellsY[2] = player.y;
        cellsX[3] = player.x;      // West
        cellsY[3] = player.y-1;
        
        // Check each adjacent square to the player for a wall
        for(var i = 0; i < 4; i++)
        {
            // If the target tile to view is on the board
            if (cellsX[i] >= 0 && cellsX[i] <= 5 && cellsY[i] >= 0 && cellsY[i] <= 5)
            {
                // If there is a wall, remove it.
                if (board[cellsX[i]][cellsY[i]] === "W")
                {
                    board[cellsX[i]][cellsY[i]] = "";
                }
            }
        }
        bombs--;
    }
    else
    {
        document.getElementById("bombs").className = "text-danger";
    }
    displayGameInfo();
    drawBoard();
    narrative += "<span class=\"text-danger\"> An ear shattering explosion resonates throughout the cave as walls around you fall away.</span>";
    narrate();
}

//  PURPOSE:    called directly from the webpage, to move the character to an adjacent table cell
function movePlayer(direction)
{
    direction = Number(direction);
    
    // Remove the player character
    board[player.x][player.y] = "";
    
    // Move player in the direction according to input
    switch (direction)
    {
        // North
        case 0:
            if ((player.x-1) >= 0 && board[player.x-1][player.y] !== "W")
            {
                player.x--;
            }
            break;
        // East
        case 1:
            if ((player.y+1) <= 5 && board[player.x][player.y+1] !== "W")
            {
                player.y++;
            }
            break;
        // South
        case 2:
            if ((player.x+1) <= 5 && board[player.x+1][player.y] !== "W")
            {
                player.x++;
            }
            break;
        // West
        case 3:
            if ((player.y-1) >= 0 && board[player.x][player.y-1] !== "W")
            {
                player.y--;
            }
            break;
    }
    
    updateView();
    pickUpItem();
    
    // Place the player character
    board[player.x][player.y] = "P";
    
    checkAdjacent();
    drawBoard();
    displayGameInfo();
    checkWin();
    checkLose();
    narrate();
}