Boardgames for the win: Monopoly

Steve Schofield
Beach.io
Published in
19 min readJan 19, 2021

--

It has been almost a year since the pandemic took hold, people were sent home and teams everywhere were forced, if they weren’t already prepared, to get to grips with a new reality.

Fast forward and as I write, the UK is in another National-level lockdown. We’re still working from home, where possible.

It doesn’t look like that situation will change anytime soon. Even with the start of a rollout of vaccines globally, the reality is that companies have seen huge benefits of remote and distributed teams and the slow macro trends of the rise of the gig-economy, movement of workers out of cities and digital transformation of the enterprise have been accelerated 7X faster as a result.

But employees, while valuing the flexibility and are increasingly seeing remote work as a key factor in evaluating new opportunities, are facing new hurdles to overcome.

The top struggles of remote workers include unplugging from work, loneliness, collaboration & communication. As a result, companies and management teams have been working on new ways to engage employees and broaden the scope of meeting facilitation to ensure that time is spent on the soft elements of work — namely relationships, mental health, support and just plain old social interaction. MURAL, where I am Head of Product for the Labs team, has certainly seen huge growth, interest and innovation in very cool new warmups, energisers, team building and social activities packaged up as reusable templates.

But, this article isn’t another review of the COVID-19 trends, despite doing an ok job of masquerading as one. Instead I wanted to explore how the visual canvas based products, such as MURAL, could be used in additional ways to foster relationships, escape from the hardcore productive work cycles, enjoy some casual competition. There is a MURAL Jeopardy template already in the template library and it got me thinking — what other games could we bring to life in the canvas? How can we leverage the existing multiplayer real-time collaborative environment? Which of the native tools could be used and which new tools would be needed in the future to bring board games to life?

For this exploration, I am going to use our product Vulcan, which is designed to enable this experimentation through its extensible features.

For Builders

What we are building…

Monopoly, canvas style

It was pretty easy to pick a game to try and reproduce in Vulcan. It’s just been Christmas and that normally means at least one occasion of my wife and other members of my family ganging up on me to beat me at Monopoly. Since the scars are still fresh (I did mention friendly competition earlier, didn’t I? Let’s scratch that…) what better example to explore for starters?

Here is the initial list I made from the top of my head of what would likely be needed:

  • A Monopoly Board
  • 2 x Dice
  • Player character tokens
  • The Bank — Monopoly money in the relevant denominations
  • Property Cards
  • Community Chest and Chance Cards
  • Buildings for houses and hotels
  • Instructions

So let’s start, though I may jump around the list order as we go…

Monopoly Board

Initially I thought about drawing the board myself, assembling some mixture of grouped rectangles into place to form our 10x10x10x10 board placements and two further placeholders for our Chance and Community Chest cards.

That didn’t last long and instead thought I’d just find an existing board image using the Bing image search integration. Fortunately there were quite a few to choose from, so I just added one as an Image Object to the canvas.

We’re off to a great start, we have our Monopoly board. But, I didn’t feel quite right — I felt like I’d cheated (which I obviously had). I wanted to make this all important first step a little more of an achievement. When I was browsing the Monopoly images that the Bing image search returned, I came across a number of Monopoly board variants — the traditional London board, a US board and even a super old board from the original Monopoly Patent back in 1935.

So I thought it would be cool if we could have an option to easily select our preferred board and that would be a great way to add a little more functionality to an otherwise trivial start.

Monopoly Board Picker

Sure we could just search through Bing images for Monopoly boards, but then we’d have to filter out the crap each time. Instead I curated a few boards, knowing that the list of ‘approved by me’ boards could be added to in future.

We will use Vulcan’s Custom Components to build our Board picker.

I’m going to keep it simple and just have a list of Cards, each of which will display an image of the board and have a button that will enable me to activate that board.

I will add the component into the Text Editor of the Image Object I already created. So with a very rough sketch, this is what we will make:

To begin with, I will use these three boards:

Left to right: 1935 Patent Board, Parker London Board, Hasbro US Board

Let’s code…

As always with Vulcan Custom Components, we start with our boilerplate Vue single file component.

<template>
<div>
<!-- Template here -->
</div>
</template>
<script>
module.exports = {
name: 'ComponentName',
props: {},
data: function () {
return {}
},
methods: {},
computed: {}
}
</script>
<style scoped>
/* styles here */
</style>

Let’s start with the Template, to create our structure.

<template>
<container>
<v-card width="360">
<v-img src="https://vulcan-marketing.s3.eu-west-2.amazonaws.com/patent-board.jpg"></v-img>
<v-card-title>1935 Patent Board</v-card-title>
<v-card-actions>
<v-btn @click="">Select</v-btn>
</v-card-actions>
</v-card>
</container>
</template>

And this is the result.

Our Board Item

So now we have a single board, but we want a list, so let’s go ahead and implement it, between our script and template sections.

Let’s add a new data property called boardOptions . It will be an array of objects, containing id, name and imageURL properties.

data: function () {
return {
boardOptions: [
{
id: 1,
name: 'Patent',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/patent-board.jpg'
},
{
id: 2,
name: 'Hasbro US',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/hasbro-us-board.jpg'
},
{
id: 3,
name: 'Parker London',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/parker-london-board.jpg'
}
]
}
}

We can now add a v-for to iterate over our boardOptions to create a list. We will use the id as as the key attribute.

<v-card width="360" v-for="board in boardOptions"     :key="board.id" class="mb-4">

Now, we have access to the board instance in each of our v-card . So we can replace the hardcoded src attribute with a dynamic value.

<v-img :src="board.imageURL"></v-img>

And similarly replace the hardcoded name.

<v-card-title>{{board.name}}</v-card-title>

We will now add a click handler method to our v-btn which we will implement after. We will call it selectBoard() and pass in the current board as a parameter of the method.

<v-btn @click="selectBoard(board)">Select</v-btn>

So in the end our template looks like this:

<template>
<v-container>
<v-card width="360" v-for="board in boardOptions" :key="board.id" class="mb-4">
<v-img :src="board.imageURL"></v-img>
<v-card-title>{{board.name}}</v-card-title>
<v-card-actions>
<v-btn @click="selectBoard(board)">Select</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>

And the rendered component like this:

For the remainder of our script we will need to implement the selectBoard() method and ensure that when the board is selected, we update the Image Object’s image property accordingly so that the change persists.

To do this, we will start with adding the selectBoard() method to the methods section of our script .

methods: {
selectBoard(board) {
// implementation here
}
}

Inside this method, we will call a new method that we will create to handle the image updating, called updateImage(). Update image will require reference to the Object that we want to update the image of and images that will be used.

//method
updateImage(object, imageURL)
methods: {
selectBoard(board) {
// call the method
this.updateImage(this.baseObject, board.imageURL)
}
}

You may have noticed that we pass this.baseObject but haven’t got a reference to a baseObject yet. This is a special reference to the current object wherever we will enable this custom component and we create this in the component props .

props: {
baseObject: {
type: Object,
hidden: true
}
}

We pass hidden: true so that this prop is not exposed on the slash command in the text editor.

In the final step, we will implement the all important updateImage() method, which will be a dispatch call to the the $store to update the object .

updateImage(object, imageURL) {
object.info.settings.image = {
body: {
imageURL,
thumb: {
url: imageURL
}
}
}
this.$store.dispatch("object/update", object);
}

Now that this is done, we are complete. You may have noticed that I haven’t written any css styling — that is because I’m using the standard Vuetify material design components built in to Vulcan’s design system.

This is the full custom component we have built for our Monopoly Board.

<template>
<v-container>
<v-card width="360" v-for="board in boardOptions" :key="board.id" class="mb-4">
<v-img :src="board.imageURL"></v-img>
<v-card-title>{{board.name}}</v-card-title>
<v-card-actions>
<v-btn @click="selectBoard(board)">Select</v-btn>
</v-card-actions>
</v-card>
</v-container>
</template>
<script>
module.exports = {
name: 'MonopolyBoard',
props: {
baseObject: {
type: Object,
hidden: true
}
},
data: function () {
return {
boardOptions: [
{
id: 1,
name: 'Patent',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/patent-board.jpg'
},
{
id: 2,
name: 'Hasbro US',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/hasbro-us-board.jpg'
},
{
id: 3,
name: 'Parker London',
imageURL: 'https://vulcan-marketing.s3.eu-west-2.amazonaws.com/parker-london-board.jpg'
}
]
}
},
methods: {
selectBoard(board) {
this.updateImage(this.baseObject, board.imageURL);
},
updateImage(object, imageURL) {
object.info.settings.image = {
body: {
imageURL,
thumb: {
url: imageURL

}
}
}
this.$store.dispatch("object/update", object);
}
},
computed: {}
}
</script>
<style scoped>
/* Nothing required here!!! */
</style>

So, now we have a Monopoly Board, complete with Board Picker and the ability to change boards depending on the game we want to play.

Player Characters

Next up, I need some Characters. This should be a quick win, and I will try to avoid any scope creep on this one after our last detour.

Monopoly Player Characters

Now, this could be a little controversial. As I was searching around for character images, using google image search with transparent option enable so I didn’t have to do any photo editing, I was missing the “Iron”. We all know the character, it looks like this:

I saw the ‘In Memoriam’ title and wondered what that was all about.

Turns out the Iron was removed after a Facebook Campaign sought to weed out the least popular character, after getting just 8% of the vote. In its place, as if ordained by the gods of the internet, the Cat took over.

I’m using the cat, because there was no Iron image with a transparent background on the first page of the search results (another kick in face by the internet age to traditional old iron, sorry).

So all in all, achieved with some primitive Image Objects. I group the collection so that I can save it as a Template for future reuse.

Dice

In order to make a move, two dice are rolled by the player. The player adds up the total on the top face of each die. The result is the number of spaces they move. If the dice show the same number on each, it is called Doubles. If the player rolls Doubles, he/she rolls again. However, if a player rolls Doubles three times in succession (in one turn), they immediately move to Jail without moving the third time.

So we need to create a way to generate two random dice faces for each roll, each dice with the standard 6 faces.

For this, I again will turn to our custom components. In this case, I will create a component that can be added directly to the game board as a custom Object type.

In preparation, I needed to create a collection of images for each of the possible combinations that could be rolled with 2 dice.

I exported each combination as a jpg file, with a specific naming convention for the file that included the values of each dice.

  • 1x1.jpg
  • 1x2.jpg
  • 1x3.jpg
  • … you get the idea

I uploaded them to a file storage service, in my case and AWS S3 bucket.

Let’s now implement the component. In the same was as before will we make another single file Vue component. Let’s start with the template.

<template>
<div>
<v-card>
<v-img :src="imgSrc"></v-img>
<v-card-actions>
<v-btn @click="rollDice">Roll</v-btn>
</v-card-actions>
</v-card>
</div>
</template>

Very simply, this will be a v-card with a button and an image. When the player presses the “Roll” button, we will call the rollDice() function and generate an image that will display the output of the roll.

rollDice () {
const rollOne = this.roll();
const rollTwo = this.roll();
this.lastRoll = [rollOne, rollTwo];
const img = this.imgSrc;
this.updateImage(this.baseObject, imageUrl);
}

This function will assign a const variable for each die, using a new method roll() .

roll() {
return Math.floor(Math.random() * this.sides) + 1;
}

We will combine the output of each die to an array, which we will store a a data property in our current state. Let’s call it lastRoll . We also pass in the number of sides that the face has. I added this as more of a utility, as I can imagine many use-cases for this Dice Roll component when I am finished, so that we could have Dice with different numbers of faces.

data: function () {
return {
sides: 6,
lastRoll: []
}
}

The next step of rollDice will then use a computed property to build the url of our image, based on the values created and stored in lastRoll .

computed: {
imgSrc() {
return this.lastRoll[0] ? `https://vulcan-marketing.s3.eu-west-2.amazonaws.com/${this.lastRoll[0]}x${this.lastRoll[1]}.jpg` : "";
}
}

So now we have created a randomised roll of the dice and returned the relevant image of the two faces of the dice. The final step is very similar, or in fact identical to the Monopoly board example as we update the Object with the new image so all of the players sees the result of their roll.

updateImage(object, imageUrl) {
object.info.settings.image = {
body: {
imageUrl,
thumb: {
url: imageUrl
}
}
}
this.$store.dispatch("object/update", object);
}

And not forgetting to add the baseObject to props .

props: {
baseObject: {
type: Object,
hidden: true
}
}

Now, since we want to use this as a Custom Object type, we need to publish the component. We do this in the fourth tab, and switch on the “Can be Chart Object” option.

As a very simple Object, we just need to decide where in the Object Picker we would like to list it. I have created a new Object Picker tab, called Games, and selected it.

Here’s the result:

Building and Hotels

Alright, after a couple of custom components, I’m up for something super simple. Let’s do the Properties pieces. I can’t even be bothered to find images for them. How about we just use some basic primitive coloured rectangles?

Monopoly Property Department

Property Cards

OK, I’ve made a cup of tea, I’m back on form. Let’s create the property cards. Once again I contemplated making them by hand with Vulcan primitives, but there’s just something quite alluring about using snapshots of old cards, the nostalgia kick is worth it.

So I found a collection of old property card photos and sliced them up and made individual Image Objects for each. I then sorted and stacked them and finally grouped them together. I made a template from the group for future re-use.

My Monopoly Estate Agent

The Bank

I’m on a roll now. I followed exactly the same approach for money, including ONCE AGAIN, considering designing my own notes and ONCE AGAIN, deciding against it.

Each note was created as an Image Object. I collected one of each together and grouped them. I saved the group as a template for future re-use.

I only created a single note, as with Vulcan it is very easy to simply duplicate (ctrl + d) the notes to issue to the Players.

I did exactly this to then create the player initial hand.

I issued 5 x $1, 2 x $10, 1 x $20, 1 x $50, 4 x $100 and 2 x $500 notes to a group and saved as a template. Using the templates makes it very easy to issue new hands to new players without any hassle of dealing individual notes.

Now, throughout these last few steps I wish I could say that I knew all of the rules off by heart, but I don’t. Instead, I had to refer to the rules. In fact, having the instructions to hand would be very useful for the players too. So let’s add a nice way to have the relevant information about the game to hand on the board.

I could just copy and paste instructions from some source into an object that has the Vulcan text editor.

I could also take screenshots of the actual instructions manual that ships with the game.

Instead I decided to use the same source that I had been referring to — the Wikipedia page.

Instructions Manual

We will build a custom component that will pull the wikipedia page into a Vulcan text editor-enabled object.

Now, this has to be the simplest custom component template ever. Here it is:

<template>
<div v-html="html">
</div>
</template>

There will be no custom styles, so we will now just focus on our script .

We will give our component a name and define a single prop that will be our Wikipedia page query. It’ll be a generic component that can be used for any Wikipedia page, so we will just set a basic default for now.

name: 'WikipediaPage',
props: {
title: {
type: String,
default: "Google"
}
}

Next, let’s create a data property where we will store the html that we want to render in the page. This will just be an empty string, until we receive this data from the Wikipedia API.

data: function () {
return {
html: ""
}
}

Now we will add the mounted() lifecycle method. This is called when the component is loaded.

mounted: function() {
const self = this;
let options = {
headers: {
"Api-User-Agent": "you@yoursite.com",
"Content-Type": "text/html"
}
}
this.$http.get("https://en.wikipedia.org/api/rest_v1/page/html/" + this.title, options).then(function (result){
let trimmed_result = result.bodyText.replace('<base href="//en.wikipedia.org/wiki/"/>','');
self.html = trimmed_result;
});
}

Here we are creating an API GET request to the Wikipedia API, passing in our query in the URL params. This is an asynchronous call using promises. The API request will be intercepted and proxied through the Vulcan proxy server before being forwarded to the Wikipedia API.

When we receive the response, we need to do a bit of clean up of some unwanted html attributes before we save the trimmed_result to our html data property.

Using Vue’s reactivity, the UI will automatically update and how the Wikipedia content.

Now if we pass in Monopoly_(game)#Rules when we use the published component we have the instructions at our fingertips in the canvas.

Community Chest and Chance Card Decks

The final step is to create the Community Chest and Chance card decks that are used whenever the player lands on the corresponding space.

I could just follow the same route and find images for all of these cards, but I think in this case, there is a better way. We should generate the cards from a single location and store all of the card details inside a custom component.

However, I will start by creating two Image Objects, to represent the top cards for each of Chance and Community Chest stacks.

Now I need some way of generating a card from the relevant stack. I think I’ll do this using a button that will trigger the action. The result will be the creation of a new Rectangle (Rect) Object, of an appropriate colour, that will contain one of the pre-determined card messages at random.

Let’s build this custom component.

<template>
<div>
<v-card width="400">
<v-card-actions>
<v-btn block color="purple" dark v-if="cardType == 1" @click="selectCard('COMMUNITY_CHEST')">Pick a Community Chest</v-btn>
<v-btn block color="orange" dark v-if="cardType == 0" @click="selectCard('CHANCE')">Pick a Chance</v-btn>
</v-card-actions>
</v-card>
</div>
</template>

In my template I am wrapping the component in a v-card so we get the card-like border and shadow effect (this is quite a standard approach, it seems). Within the card we are including two buttons, which will only be shown depending on the selected cardType . This way, we can create a single component, but just select at the time of using what type of card — Chance or Community Chest, will be used. Finally we set a color and add the block attribute so the button fills the full width of the card. We also add a @click handler for the buttons, which will call a selectCard() method and pass in the type of card as an argument.

On to the script . Let’s start with the basic setup.

name: 'MonopolyCard',
props: {
baseObject: {
type: Object,
hidden: true
},
cardType: {
type: Number,
default: 1
}
},

We are now very familiar with naming our component and adding the baseObject prop. We will need this so we can once again generate new objects from the Object that will be created for our Custom Component.

Additionally we add the cardType prop. This will be provided at the time of creation by whoever sets up the game, to select whether it is a Chance or Community Chest type. We will create two instances of this object when we set up the game — one for each type. We already know from our template that if

cardType is 0 then it is a ‘Chance’

cardType is 1then it is a ‘Community Chest’

Moving on to our data this is where we will add all of the various messages, in each of two arrays.

data: function () {
return {
communityChest: [
"Advance to Go (Collect $200)",
"Bank error in your favor—Collect $200",
"Doctor's fee—Pay $50",
"From sale of stock you get $50",
"Get Out of Jail Free",
"Go to Jail–Go directly to jail–Do not pass Go–Do not collect $200",
"Grand Opera Night—Collect $50 from every player for opening night seats",
"Holiday Fund matures—Receive $100",
"Income tax refund–Collect $20",
"It is your birthday—Collect $10",
"Life insurance matures–Collect $100",
"Pay hospital fees of $100",
"Pay school fees of $150",
"Receive $25 consultancy fee",
"You are assessed for street repairs–$40 per house–$115 per hotel",
"You have won second prize in a beauty contest–Collect $10",
"You inherit $100"
],
chance: [
"Advance to Go",
"Go to jail. Move directly to jail. Do not pass Go. Do not collect £200",
"Advance to Pall Mall. If you pass Go collection £200",
"Take a trip to Marylebone Station and if you pass Go collect £200",
"Advance to Trafalgar Square. If you pass Go collect £200",
"Advance to Mayfair",
"Go back three spaces",
"Make general repairs on all of your houses. For each house pay £25. For each hotel pay £100",
"You are assessed for street repairs: £40 per house, £115 per hotel",
"Pay school fees of £150",
"Drunk in charge fine £20",
"Speeding fine £15",
"Your building loan matures. Receive £150",
"You have won a crossword competition. Collect £100",
"Bank pays you dividend of £50",
"Get out of jail free. This card may be kept until needed or sold"
]
}
},

In our methods we will implement our selectCard() which will fire whenever someone clicks our button. We will also add a generateCard() method which will be responsible for spawning the new object that contains the card message.

selectCard(type) {
switch (type) {
case 'COMMUNITY_CHEST':
this.text = this.communityChest[Math.floor(Math.random()*this.communityChest.length)];
this.color = 'yellow';
this.generateCard();
break;
case 'CHANCE':
this.text = this.chance[Math.floor(Math.random()*this.chance.length)];
this.color = 'pink';
this.generateCard();
break;
default:
break;
}
},

I decided to use a switch I really just prefer them to if else for readability) where we will conditionally handle each type of card.

We reuse our random selector algorithm from earlier to pick a message from the stack to set the text.

After setting the color we then go ahead and call generateCard() to create the card of the appropriate type.

generateCard() {// get the position of our base object 
const { x, y } = this.baseObject.position;
// get the width of the baseObject or default to 100
const { width } = this.baseObject.size || {width: 100};

// decide an offset for positional purposes
let offset = 100;
// get the text and color from state
const text = this.text;
const color = this.color;
// create the position for the new object based on our current object
const position = {
x: x + width + offset,
y: y + 1 * 200
};
// async promise call to $store to generate object
const objectPromise = this.$store.dispatch("object/create", {
type: "Base_RectObject",
position,
size: {
width: 300,
height: 200
},
info: {
settings: {
content: text,
backgroundColor: color
}
}
});
}

And there we have it. As a result we have two stacks for community and chance and the ability to draw a random card from either stack.

In future, we could add a way to also persist information about what cards we have used and ensure that we remove the cards from the stack so that they can’t be reused.

The result

So after a few hours we have a pretty complete game of Monopoly, that I can now play with my Mum or teammates around the world.

--

--

Head of MURAL Labs. Entrepreneur, family guy and adventure sports nut.