Skip to content
15 Jun 2022

Card Draw Simulator part 2

This is written with people who are learning programming in mind, and might not make sense if you don't know what functions, arrays, and objects are.

Part 1 was context of plans and a list of features I want to build. Let’s get into some detail on the latter!

1. Present a list of available decks, and allow the player to select which one they draw from.

I’m not much of a designer, but let’s say what the player sees in their browser should look like the below mockup, and when they click one of the buttons, the deck is displayed.

Along the top edge are 4 navigation links rendered as rectangles with labels "novice ailments", "intermediate ailments", "forest" and  "loch". Next are two bigger rectangles with rounded corners, the left labelled with "Deck - click to draw", the right with a bolded headline  "Cardname" and not-bolded text  "infotext" in the top left, and  "source:..." in the lower right corner. Below the deck are three cards stacked so that only the cardtitle is visible. Right of this, below the displayed card content, is a button saying  "Shuffle drawn cards back into deck".

What’s that under the hood?

HTML & CSS

The main thing I wonder about: should there be one display area with a deck, card, and discard pile that gets updated when changing to another deck, or should each deck get its own “tab” and the tabs for inactive decks get hidden?

I think I prefer going with multiple tabs, because then I can hide and show again each tab as one thing, instead of having to change card content and list of already discarded cards one by one.

JavaScript

Data for decks

That’s where all the actual information is stored.

A list of available decks, that’s obviously an array. No wait! Text keys would be better, so in JS that’s an object. That way I can give an understandable ID in text form to each deck/tab, and use it everywhere where the question is which one is active. That’s better than numbers.

Each deck is an object with a name and a list of cards it contains — that list works as an array, since we don’t want to choose cards, just pick at random. I’ll also put the draw function into the class/prototype.

But I need to consider other features here: Keep track of which cards have already been drawn, and which are still in the deck, and let the player shuffle cards back into the deck. So we need two lists, one with all cards (to start with and to go back to after shuffling) and one with all cards that have not been drawn yet (since the last shuffle).

Here's my first go at a class representing the deck:

Class Deck {
	constructor(cards) {
		// array of objects each of which represents a card
		this.allCards = cards;
		// set up the deck ready for drawing
		this.shuffle();
	}

	// Puts already drawn cards back into the deck.
	// Note that despite the name this does not actually 
	// put the cards in a random order. 
	shuffle = function() {
		//Make an array containing only numbers from 0 to 
		//the number of remaining cards
		this.drawableCards = [];
		for (let i = 0; i > this.allCards.length; i++) {
			this.drawableCards[i] = i;
		}
	}

	draw = function() {
		//Which card will we draw from the remaining cards?
		const drawThisCard = Math.floor(Math.random() * this.drawableCards.length);
		//Remove that card from the remaining cards.
		this.drawableCards = this.drawableCards.splice(drawThisCard, 1);
		//Return the data for the card
		return this.allCards[drawThisCard];
	}
}

The draw function does not work like that. Let me illustrate. Let’s say this is the allCards array:

[ 0 : CARD_A, 1 : CARD_B, 2: CARD_C, 3: CARD_D ]

So the drawableCards array looks like this:

[ 0 : 0, 1 : 1, 2: 2, 3: 3 ]

The key is just the position in this array, the value is the key in allCards, which we need to access the actual content of the card.

Now the random number generator gives us a 1 for drawThisCard, so we remove the entry with the index 1 from drawableCards, and the remaining numbers get re-indexed:

[ 0 : 0, 1 : 2, 2: 3 ]

And we return allCards[drawThisCard], meaning allCards[1], so that’s CARD_B.

Let’s say the RNG gives us 1 again. We’d like to get CARD_C from that, because CARD_B has already been taken.

So we return allCards[drawThisCard], meaning allCards[1], which is CARD_B again. This shouldn’t happen.

The solution: We need to return allCards[drawableCards[drawThisCard]]! But we need to do that after removing that entry from drawableCards. I’ll also rename drawThisCard to drawableIndex, which tells me more clearly what it represents when I come back to the code later.

draw = function() {
    //Which card will we draw from the remaining cards?
    const drawableIndex = Math.floor(Math.random() * this.drawableCards.length);
    // remember which entry in allCards to return
    const returnedCardIndex = this.drawableCards[drawableIndex];
    //Remove that card from the remaining cards.
    this.drawableCards = this.drawableCards.splice(drawableIndex, 1);
    //Return the data for the card
    return this.allCards[returnedCardIndex];
}

That’s the basic functionality sorted.

What’s missing is the actual cards. I will be putting all of them in an object whose property names are the deck IDs, and the values each an array with objects representing one card each inside. That way there’s no need to make code changes like adding more variables when adding new decks.

const deckData = {
    deckID: {
        [{
            name: "…", // name/title to identify the card to the player
            text: "…", // detail information for the player to read and act on
            source: "…" // which rulebook, expansion or other source it was published in
        },
	…
    }
}

DeckID would for example be NoviceAilments, IntermediateAilments, Village,… easily human-readable. Each card is just an object storing strings, no methods.

DeckData is a global variable.

Example for creating a particular deck:

Const deck = new Deck(deckData[‘NoviceAilments’]);

Choosing a Deck

A deck should be set active when its corresponding navigation link is clicked.

Here’d the HTML for the navigation:

<nav>
	<a href="#NoviceAilments" style="ailments">Novice Ailments</a>
	<a href="#IntermediateAilments" style="ailments">Intermediate Ailments</a>
	<a href="#Village" style="location village">Village</a>
</nav>

The anchors are identical to the deckIDs used in the deckData object! Now I add a click event listeners to all of them:

let elements = document.querySelectorAll("nav a");
for (const elem of elements) {
    elem.addEventListener('click', event => {
        const deckId = elem.getAttribute("href").substring(1);
        setActiveDeck(deckId);
    });
}

Now only the setActiveDeck() function that does the actual work is missing. I don’t create all deck objects up front, but create each when it is used the first time. They are all saved in the global variable decks.

function setActiveDeck(deckId) {
    if (deckData[deckId] === undefined) {
        alert("Sorry, this deck is missing");
        return;
    }
    if (decks[activeDeckId] === undefined) {
        decks[activeDeckId] = new Deck(deckData[activeDeckId]['deck']);
    }
}

Doesn’t that create redundancy?

A bit. The list of cards in the individual deck object is a reference to the list in the deckData object, not a true copy.

But on second thought I see you could skip the entire list of cards in the deck object and instead of this.allCards[returnedCardIndex] return decks[deckId][returnedCardIndex]. I’m leaving it as-is right now, because it does what it should, and think about it later, if I add more features.

Nothing gets displayed but the navigation...

Yeah, it turns out writing up things like that takes more time than actually doing it! Here's a screenshot of what I have at this point:

There are plain text links reading "Novice Ailments", "Intermediate Ailments", "Village" and "Forest" along the top. Next black a rectangle with rounded corners with "Novice Ailments" written on it, and below that a bigger, grey rectangle with rounded corners reading "Phodothropy [CURSE 1] [HAIR 1] – Timer: 6 During the fullmoon, the afflicted turns into a monstrous hamster and scurries around their home and village, nibbling on carts and walls. Consequence: They go on another rampage. Lose 1 Reputation. Describe the aftermath. Apothecaria"

This is the only non-title text from the game I'll share, for hopefully obvious copyright reasons. It should give you an idea of the tone of the game - it's great fun and I recommend it! You can find the Apothecaria PDF on itch.io or the PDF or a print version on blackwellwriter.com

Next time I'll focus on getting things actually displayed. If you have questions, please ask!

You can download the code as it is at this point as a zip file.

Tags:
end of the line
Content tagged "Year 2022"
edit
I agree with the Privacy Terms

no comments yet