StarPlay

Introduction

After some thought of how to use Nintendo's amiibo in a game of my own I came up with my own electronic board game StarPlay which uses amiibo figures as interactive board pieces as well as a number of my own custom game pieces. An amiibo other than the toy part itself is nothing more than an NTAG215 NFC tag which has its own rewritable memory which is 540 bytes in size and the memory is split into pages with each page being 4 bytes. For more information about NTAG215 please see the datasheet:

https://www.nxp.com/docs/en/data-sheet/NTAG213_215_216.pdf

You can easily get information about your own amiibo using a smartphone with NFC and an app such as NFC TagInfo (Android/iOS).

Please note that StarPlay saves custom data to amiibo which will overwrite data that was previously stored on the amiibo. If you use amiibo with StarPlay and then with an official Nintendo game you will need to restore the amiibo.

This project is very much a prototype and as such there is much that could be done to improve it; I would very much like to hear your ideas. You can watch a video I did about StarPlay which includes looking at feedback from a tester:

https://youtu.be/DlHH_gEBcIc

How to play

StarPlay is a game for 2 to 4 players, the aim of which is to collect the required number of star points and then land on the end square. Stars are bought using coins picked up on the board much like in a Mario party game. However, through the support of Nintendo's NFC figures StarPlay allows players to register amiibo so that players are referred to by their amiibo name and their winning stats are saved to each amiibo at the end of the game. As well as support for amiibo the game also supports custom NFC tags (NTAG215) in the form of a confirm, reject and 'card' tag. In addition, an LCD explains how to play the game and there are coloured lights on every square (red for P1, green for P2, blue for P3 and white for P4 based on the order the amiibo were registered) so there can never be any arguments as to what square someone should be on.

The board

Let's look at the board layout:

The home square is the only part of the game board that detects amiibo and other tags and is represented but a very simplified home symbol; above the home square are 4 amiibo starting squares (each square contains a circle) to place up to 4 amiibo before the game actually starts. When the turn order has been decided the amiibo of the player to go first should be placed on the start square with the other amiibo following behind and all amiibo can move forward as each player takes their first turn until they are all on the main part of the board, moving clockwise around the board. The above layout doesn't include outlines for the LCD, LED display and 2 switches but does have an outline for the card tag. After a player has finished the amiibo can be placed back on the starting squares.

The Circuit

The circuit for StarPlay is not that complex but there are a number of elements that make the game possible with an Arduino Mega as the 'brains' of the system as you can see below:

The Arduino Mega was chosen because of its large number of GPIO and as we can see from the schematic above everything else attaches to the Mega which also supplies power via its 3.3V and 5V power supply connections. To interface with amiibo and the other game pieces a PN532 NFC module has been used which communicates with the Mega via its I2C interface but through a TXS0108E bi-directional level converter as the PN532 appears to use 3.3V logic levels. The game uses 4 switches, with S1 being used to start a new game, S2 to roll the virtual dice, S3 to enter test mode and S4 to activate program mode. As for the LCD (LCD1) which is a standard HD44780 character LCD module it is operated in 4-bit mode through use of the Mega's digital pins D6-D9 and D11 and D12. You will need to adjust preset variable resistor PR1 to alter the LCD''s contrast and the LCD's backlight limiting resistor R1 may need to be adjusted to better suit your LCD and be sure to check that the pin numbering shown on the circuit matches your LCD.

For indicating the dice roll value a 7-segment common cathode LED display (LD1) has been used but depending on your specific display you may have to use a different value for limiting resistors R2-R8. Lastly we have 20 LED's, LED1-LED20 which are of type APA-106 (a type of NeoPixel clone) with each LED capable of lighting a huge range of colours using just three wires each and only needing each LED to be daisy-chained. Capacitor C1 provides a boost for the LED's along with 10 more capacitors C2-C11 (0.1uF each) to act as decoupling capacitors for each LED pair but if you have issues with the LED's you may want to use a decoupling capacitor on each individual LED. Note that a limiting resistor, R9, is connected from the Arduino's digital pin D19 to the DI of LED1 to protect the LED by helping to prevent voltage spikes; the resistor should be placed as close to the LED as possible. It is also worth mentioning that upon power up the LED's will light up a random colour (fortunately the Mega is able to provide enough power) but if after a few seconds an LED lights red then likely there is a wiring issue or more decoupling caps need to be added.

The Code

The sketch for the Arduino Mega is attached to the bottom of this page in the zipped file 'StarPlay_board_game' as 'Amiibo_board_game.ino' and needs to be downloaded, extracted and transferred to the Mega. Please note that the code is far from perfect and there is need for optimisation and there may still be some bugs. Near the start of the sketch is a listing of the hardware set-up followed by the library includes; 'Wire.h' for 12C communication, 'Adafruit_PN532.h' for the NFC module (https://github.com/adafruit/Adafruit-PN532), 'Adafruit_NeoPixel.h' for the APA-106 LED's (https://github.com/adafruit/Adafruit_NeoPixel) and 'LiquidCrystal' for the HD44780 LCD. Next we have constants and they are followed by the variables which includes class instances: NFC module as 'nfc', LCD module as 'LCD' and APA-106 LED as 'Neo_LED'. In the setup() function we open the serial port and while the serial port is used for debugging most code that outputs to the serial port has been commented out but of course can easily be put back in if needed. Also in setup() we initialise classes/structures, check for the NFC module, set up the LCD module and switches, and lastly output the introductory message to the LCD. I took a simple approach to the LCD in that text is always displayed from the first character position and there is no ability to move to a new line. There are two functions for outputting text to the LCD which are outTextLCD_Delay() to output the text to the LCD with a set delay determined by textMinDelay multiplied by the number of characters in the text string and outTextLCD() which adds no delay.

The loop() function will call checkForNewGame() and updateGame() as long as we are not in test mode (testModeOn = false) and not in program mode (progModeOn = false) but checkForTestMode() and checkForProg() are called regardless to check for and deal with test and program modes. In checkForNewGame() the new game button is checked (button press is read as false and initernal pull-up resistors were enabled in setup()) which will call newGame() to start a new game if a game is already running (GAME_IN_PROGRESS == gameState) or if the game is waiting to start (GAME_WAITING == gameState). The newGame() routine sets up all players (maximum of 4) by resetting the players structure players[], initialising the random number generator using time from millis() and setting the default values for various other variables. We call disTimerInt() to disable the timer interrupt should the game already be running; the timer interrupt is used so we can cycle through colours on an LED for the start square at the game beginning or when 2 players land on the same square, independently of anything else we may be doing and to make the timing of the flashing easy. Next we blank the LED display segments by calling clearLED_disp() and we turn off the LED's using function LED_neo_off(). Note that if you connect the Mega to a PC there may be some delay before the LED's turn off due to the wait for the Arduino to appear as a serial port on the PC. The last things we do in newGame() is set the game state to GAME_REGISTER so that the player amiibo can be registered and a message is displayed on the LCD asking players to register their amiibo.

Back to loop(): after checkForNewGame() we call updateGame() which checks the game state (variable gameState) and calls the appropriate functions. When gameState is set to GAME_REGISTER (registering amiibo) function doGameRegister() is called in which the code will loop (the while loop) until 2 or more amiibo have been registered (for 3 or less players the confirm tag is used to confirm the number of players). To look for an amiibo or other tag we call checkForAmiiboOrConfirm() which takes the player index (playerTotal is used as effectively that will be the same thing) so that should an amiibo be found we can save its UID (Unique IDentifier) to the player structure as when it comes to saving the win stats we want to save the data to the correct amiibo. Looking at checkForAmiiboOrConfirm() the function calls checkForTag() which in turn calls getTagUID() which calls nfc.readPassiveTargetID() to actually look for a tag. The code in checkForTag() checks that the UID length is correct and if it is then checkForAmiiboOrConfirm() can attempt to get the character ID (the type of amiibo) by calling getCharID_from_tag() which tries to read the character ID from the amiibo using getPage() (the character ID spans 2 pages from page 21) and once we have the character ID we can get the character index (a single digit representing the character) through use of getCharIndFromCharID() which uses 2 loops to find the character index in charID[] via doCharID_Match(). Array charID[] simply lists each character ID starting with the custom tags first (confirm/reject/card) whose ID starts with 0xFF (as to not clash with amiibo character ID values) followed by the amiibo character ID's. The game can only support amiibo that have been added to charID[] and it would require a large database to support all amiibo. Back to checkForAmiiboOrConfirm(): once we have the character index we return it if an amiibo or confirm tag is detected otherwise 'NO_PIECE' is returned if the amiibo wasn't recognised (note that the game will wait indefinitely for a tag as there is no timeout with the default settings). Then in doGameRegister() if a tag was detected we can either exit the while loop by setting registering to false if the confirm tag was detected and at least 2 players have been registered or if an amiibo was detected we save the character index using setCharInd() and output a message to the LCD confirming that the amiino has been registered. If 4 players have been registered then registering becomes false so we can exit the while loop and the LCD confirms the number of players and sets gameState to GAME_IN_PROGRESS before calling detPlayerOrder() to determine the turn order. In detPlayerOrder() we loop through the number of players registered and try to randomly generate a random turn order, remembering previously selected ordering using plyPosCheck[] which contains an entry for 1st, 2nd, 3rd and 4th. If one of those positions hasn't been taken then it will be false and if the position has been taken then it'll be true. If we do generate a random turn order that has already been taken then we keep generating more turn order numbers until we get one that hasn't be taken. When the turn order has been decided for all players the turn order is announced on the LCD with 2 players at a time on a single 'page'. Toward the end of detPlayerOrder() we enable the timer interrupt by calling enTimerInt() and set LED_cycle_start_player to true, a message is put on the display to prompt the players to line up their amiibo ready and while the text is displayed the interrupt at SIGNAL will alternate the player colours on the start square. After the text delay the interrupt is disabled and the start square LED is turned off.

In updateGame() if gameState is set to GAME_IN_PROGRESS (which it will be after registering players) the game loops through all players and if the player hasn't already finished the game (finishPos is 0) and the player is not to miss their current turn then the current player is prompted to press the roll button. Then function rollDiceMovePlyWithAct() is called which can move a player forward or back by a random number of spaces. First, the routine checks if the player is to miss their turn and if so provided they haven't landed on another player's square (playerConflict is false) the LCD confirms the player will miss their turn and then the 'miss a turn' mechanism is disabled using setMissTurn() and passing false. If the player is not to miss their turn then getDiceRoll() returns a random roll value by waiting for the roll button to be pressed (the switch is debounced using debounceSwitch() which can operate on only one switch at a time) and while the roll switch is not pressed the LED display cycles through all numbers at high speed. When the roll switch is pressed a random number is generated but 0 is only possible as an outcome if parameter incZero is true and there the player is not another someone else's square. The randomly generated value is outputted to the roll display and the value is returned by getDiceRoll(). The roll value is then acknowledged on the LCD back in the rollDiceMovePlyWithAct() routine and as long as a 0 wasn't the roll value the player is moved on the board using movePlayOnBoard(). Function movePlayOnBoard() gets the player's current position using getBoardPos() and calculates the new board position; rollDiceMovePlyWithAct() will pass a negative value to movePlayOnBoard() if the player is to move back. A check is made in movePlayOnBoard() to see if the player has gone past the end square and if so if they have enough star points to win the game they are warned they need to land exactly on the end square and thus can't move. If the player has gone past the end square but is not in the position to win then their position simply wraps round. If the player is to move back and they go beyond the start square their position wraps round. Once we have a new board position the LED at the old position is turned off and the LED at the new position is turned on with a colour representing the player, the new board position is saved using setBoardPos() and the LCD confirms the square the player landed on. In rollDiceMovePlyWithAct() after calling movePlayOnBoard() we then use checkForOwnedSqr() to see if the player has now landed on another player's square and in checkForOwnedSqr() we start by getting the board square type and if the player has landed on the end square we check if the player can win by calling playerCanWin() and if so the function exits. If not we check the current player against all other players and if we find 2 players on the same square then the LCD announces what has happened and asks the player to press the roll button. Now variable playerConflict becomes true, the timer interrupt is enabled and the interrupt will flash the board square LED the colour of the 2 conflicting players. Once we get a roll value if the value is even then the non-current player will have to pick a card which will cause a random action and then the non-current player will have to move back a random number of places. However, if the roll value is odd then the current player will have to pick a card and then move back a number of spaces. The function doPickCard() will wait for the card tag by calling checkForCard() and once the card tag has been detected an action will be randomly generate which can be either a free turn (CARD_FREE_TURN), miss turn (CARD_MISS_TURN), buy star for 200 coins (CARD_STAR_200), roll back (ROLL_BACK), gain 75 coins (COIN_75), loose 25 coins (COIN_MIN_25) or random coins (COIN_RAN). Lastly in rollDiceMovePlyWithAct() we call doBoardSqrAction() which will get the current board square type and call the appropriate function for the type of square that was landed on.

To try to buy a star the routine tryToBuyStar() is called which starts by finding out how many coins the current player has and then checks to see if the player has enough coins to buy the star which too low amount resulting in the LCD message telling the player they do not have enough coins to buy the star and the function exits. However, if the player does have enough coins then the player is prompted to either use the confirm tag to buy the star or the reject tag if they would rather not buy the star. The function then enters a while loop and only exits when either the confirm or reject tag is presented; if the reject tag was detected then a message appears on the LCD confirming that the star will not be bought and the routine will end. Otherwise if the confirm tag was sensed then the player's new coin total is calculated and stored as well as the player's new star point total, the values of which are displayed on the LCD. The last thing to do in tryToBuyStar() is check to see if the player now has the required number of star points to win and if so the player is informed by way of the LCD.

Returning to updateGame() should gameState be GAME_ENDED, which will happen in doEndSquare() when curFinishPos becomes the same value as the player total (playerTotal), function saveWinStatsToAmiibo() is called. This code will enter and stay within a while loop until the winning stats have been saved to all amiibo used during the game. The data is saved as follows:

Page 0x04 [0][1] total star points when game ended (2-byte value)

Page 0x04 [2][3] total coins when game ended (2-byte value)

Page 0x05 [0] number of times placed 1st (1-byte value)

Page 0x05 [1] number of times placed 2nd (1-byte value)

Page 0x05 [2] number of times placed 3rd (1-byte value)

Page 0x05 [3] number of times placed 4th (1-byte value)

Page 0x06 [0][1][2][3] Value 0x3CA96B1E

The total star points and total coins are saved just for the game that was played whereas the number of times placed 1st, 2nd, 3rd and 4th increase with each game played. The value at page 6 is used as an identifier so we know if our game has saved to the amiibo before as it will be very unlikely that an amiibo will have the value 0x3CA96B1E already stored there (another value could be used by the same value must always be used). If when saving the win stats to an amiibo we see the value is not already there we assume that we haven't saved to that amiibo before and so we save the total star points and coins but for the number of times placed... we save zeroes. If we do find that unique value at page 6 then instead of saving zeroes for the number of times placed... we read the values in, increase one of them as needed and save back.

When saving to an amiibo we check that the UID of the amiibo detected matches the one we are looking for which was previously saved when the amiibo were registered (out_amiiboNameSave() prompts the player to save their win stats) but the code could be modified to use the UID of the amiibo detected as a look-up rather than have a set order of saving to the amiibo. Because we check the amiibo's UID the game supports multiple amiibo of the same type, however, during game play it would get confusing having players with the same name. If the wrong amiibo is detected then the LCD will display a message saying that the wrong amiibo was presented. If we do detect the correct amiibo then we generate the password for the amiibo so we can write to it (reading from an amiibo doesn't require a password) and the password is based on the amiibo's UID with the password being stored in pswd[]. The function ntag2xx_Authenticate is used to present the password to the amiibo and if successful we can read in the necessary pages and save data using writePage(). At the time of making StarPlay ntag2xx_Authenticate() was not officially part of the library even though there was a commit to add the authentication function so I manually added it myself. You will find the updated .cpp and .h files in StarPlay_board_game.zip at the bottom of the page; replace the downloaded files with the updated files after you install the PN532 library.

At this point I should mention that we can only get the password wrong 7 times before the amiibo locks out but each time we get it right the password error counter will reset but isn't a problem for use as we know the password anyway. Also, I've found that often the first read or write to an amiibo fails and then further attempts are successful so rather than not moving on to the next read/write because of a failure we could just try the read/write again but with the code as it is a function will just exit if there is a read/write failure.

Next we will look at the checkForTestMode() function: we use a boolean flag, testModeOn, to remember if we are already in test mode but if we are not we check if the test mode switch is on (binary 0) and if so set to true and output to the LCD a message informing the user that test mode is active. Then, next time checkForTestMode() is called and we find that testModeOn is true we make sure that the testModeOn switch is now off which will cause testModeOn now to become false and test mode to turn off. Otherwise we call getDispTagInfo() which first waits for a tag by calling checkForTag(); once a tag has been detected we get the tag's UID by way of routine getTagUID() which returns the UID and the UID length. If we find that the UID length is correct we output the UID as a hex value to the LCD (there's a little extra bit of code to add in a leading zero if the UID value is less than 16). Next we output the character ID and we do this by using getCharID_from_tag() which will display on the LCD the character ID as a hex value as well as the character name if found.

The function checkForProg() operates similar to checkForTestMode() but updates variable progModeOn based on whether the program switch is on and when we enter program mode we output a message to the LCD explaining briefly how to use program mode. The roll switch is used to cycle through the types of game pieces we can program a tag as (confirm/reject/card) with variable progGamePieceType remembering which game piece type is currently selected. If while in program mode we detect that the new game switch has been pressed we call function progTagAsGamePiece() which first waits for a tag by way of checkForTag(). When we have detected a tag we program the character ID of the currently selected game piece type using 2 write operations for both parts of the character ID and if both writes were a success then a message is displayed on the LCD to inform the user that the tag was programmed. Note that amiibo cannot have their character ID reprogrammed.

Construction

Without having to build a box myself to act as the game board and house the electronics I came across a reasonably priced art box which opened up and had the top and bottom on hinges which would be handy for servicing. The box measures 38.5 x 30 x 4.5 cm and although a bit too small resulting in the board squares being not as big as I would have liked (a board square can barely accommodate one amiibo) once the internal drawer was removed it did the job quite well. For the board layout I printed the design using an A3 printer in black and white (colour would have been preferable had I had more time) and I used book seal to protect and hold the layout in place but anyone who has used book seal even on a small book knows how difficult it is to get the seal on neatly. An alternative would have been to laminate the printed layout and fix it into place on the top of the box or the box itself could have been painted.

For the 'new game' and 'roll' switches I used a couple of heavy duty push switches which are bolted in place and the wires are screwed in (it's a good idea to reinforce thin wire by soldering to metal pins). Originally I was to have the LED's poke through the top of the box but they would have got in the way of the amiibo so instead I hot glued them half way in the box so that they shine through the layout which helps to soften the otherwise very bright lights. Because the NFC module must be aligned well with the home square I temporary fixed the module into place and moved it until I was happy the amiibo and tags were getting detected in the correct area. Speaking of the tags, the NTAG215 tags were sandwiched between two types of emoji felt icons (smiling face for 'OK' and sad face for 'reject') and I placed an NTAG215 tag on the back of a 3DS AR '?' card (would have been better to have sandwiched the tag between 2 identical cards).

Trying to keep the wires short between the various parts I have the Arduino located near to the level converter (which is in turn connected to the NFC module) and had the Arduino lined up with one edge of the box with an area cut out so that a USB or DC power cable could be connected. Because the 7-segment LED display was soldered to a small board with the limiting resistors I was able to screw the board in place through holes made in the board. Fortunately the LCD module had mounting holes but I still soldered a small board to the LCD mainly for the contrast adjustment and backlight limiting resistor. The last thing to mention are the 'test' and 'program' switches which were soldered to a small circuit board and fixed into place near the Arduino.

Testing

Before even powering up check over all connections making sure especially that the power connections are correct and that no wires are loose or shorting and when you are happy that is all good you can then apply power to the Arduino which should cause the LCD to come to life and the LED's should turn on briefly and then off; if any of them stay on as red then likely there is fault with the LED wiring or you may need to add a decoupling capacitor to all LED's. If you are running the Arduino off a PC's USB port it can sometimes take a short while for the Arduino to appear as a device to the computer which causes a delay before the Arduino sketch runs meaning the LED's will take longer to turn off and the welcome text will take longer to appear on the LCD. If you find that there is no text on the LCD or that it's difficult to read you will need to carefully adjust variable resistor PR1 while the system is powered up until the text is readable.

If the board square LED's turn off and the welcome message is shown on the LCD then play through the game making sure that:

The switches respond (new game starts a new game and roll stops the cycling through numbers on the LED display to choose a roll number),

The LED display cycles through numbers at high speed and stops on the chosen number when the roll button is pressed; make sure numbers are displayed correctly.

There are no missing characters on the LCD; it's normal for the words to spill over multiple lines because of the limited number of characters that can be displayed.

The board square LED's light the appropriate colour for each player (in order their amiibo was registered) and are only lit while a player is on a particular square, with the LED flashing between multiple colours when a player lands on another player's square and the start square LED will cycle through all player colours at game start.

The home square responds to amiibo and the confirm, reject and card tags; if not check the positioning and wiring of the NFC module - remember to use the level converter.

As well as the testing mentioned above if you power up with the test switch in the on position the game should enter test mode in which a message should display confirming that test mode is active and if an amiibo or other game piece is placed on the home square the tag's UID, character ID and name will be displayed. If instead the game is turned on with the program switch in the on position the game will enter program mode so that you can program blank NTAG215 tags as either a confirm, reject or card tag. When in program mode use the roll button to cycle through the available game piece types and to program the tag put the tag on the home square and press the new game button.

Improvements

As it is StarPlay is a fun game for 2 to 4 players that makes use of amiibo figures but there are a number of things I would have liked to have added or changed if there had been more time, which we will look at now:

Larger board squares which have enough space for 2 amiibo.

Use of a graphics LCD instead of a text LCD so that smaller but more readable text could be used without splitting words across multiple lines.

Attract mode: when not playing each board square light turns on in turn and each segment of the LED display turns on and off showing that the lights and display are working and connected up correctly.

Sound effects when move on the board, when land on another player's square, and so on.

The board game to run off batteries, perhaps rechargeable via USB.

Support for more amiibo: to support all amiibo would require a large database perhaps stored on an SD card. An alternative would be a facility to register new amiibo and be able to name them perhaps with an attached keyboard.

Multiple 'card' tags giving the impression of chance rather than just 1 tag that randomly picks an action; that is, each card tag has a fixed action associated with it.

At any time the current game can be terminated and a new one can start (the game will ask for confirmation as to whether to start a new game).

All content of this and related pages is copyright (c) James S. 2018