TOCHORIS
Don't you think array of windows on a building as a dot display? Imagine that a skyscraper as an arcade game machine and playing PONG with it. TOCHORIS is a tetris-like puzzle game run on a miniature scale Tokyo Metropolitan Government called "Tocho". This device is created with Arduino (ATmega328P board) and full-color RGB LED matrix (array of WS2812-2020).
(Special Thanks)
Here's the websites I referred to create this artwork.
https://create.arduino.cc/projecthub/john-bradnam/...
Supplies
- Arduino (ATmega328P)
- WS2812-2020 (8x8 matrix) x 2
- WS2812B x 2
- DFPlayer Mini (mp3 decoder & 3W speaker amp.)
- TM1640
- 7-segment LED x 2
- Push button (tactile switch) x 5
- Resistor (100Ω x 8, 1kΩ x 2, 10kΩ x 3, 2.2kΩ, 4.7kΩ, 5.6kΩ, 33kΩ)
- Speaker (4Ω/8Ω)
- AAA battery x 3
What Is It Inspired By?
This artwork is inspired by "Tokyo alert", which warns the epidemic risks and information of COVID-19 in Tokyo announced by Tokyo Metropolitan Government, and it's called "Gaming Tocho (Tokyo Metropolitan Government)" over social media because of its glowing color and amusing appearance.
When I found it, I remembered some creative projects using windows on a building as a dot-matrix display to enable people to play video games like Pong or Tetris, that is, making a building as an arcade game. Then I conceived an enjoyable creative work that reproducing them with a scale model of Tokyo Metropolitan Government building instead of remodeling itself.
Electronic Circuit
First, I'd like to show the schematics of the circuit.
Basically the circuit of what I developed is the same as the picture, but I used Arduino Pro Mini (5V, 16MHz) instead of Arduino UNO to make it smaller as possible. My circuit is divided into two 2.8" x 1.87" (7.2 x 4.5 cm) PCB boards, one is the main board with MCU (Arduino Pro Mini), DF player Mini, and buttons, and the other is implemented an LED driver TM1640 to control 7-segment LEDs.
I will explain the detail of each component in the several following steps. If you don't need any explanation about the specification and description of every component, skip to the step 7.
DF Player Mini (MP3 Player)
DF player mini is a tiny MP3 decoder & 3W speaker amp. This module loads mp3 data from a SD card.
It has 3 control modes: IO control, A/D button control, and serial communication mode to play the music. In this project, I chose the 3rd one because microcontrollers can operate it only in this way.
To utilize this module from the microcontroller (Arduino), I also adopt a library "SoftwareSerial", which imitates serial communication (UART) with general IO pins to connect and communicate between them. I defined the pin 5 as receiver (RX) and pin 4 as transmitter (TX) connected to the peripheral module. Notice you do NOT connect them to the pins having the same name, but alternate with each other (pin 5 to TX, and pin 4 to RX of DF player mini).
Plus, 3 AAA batteries connected in series supplies its power (common with the main power source of the whole system), and a small piezoelectric speaker connected to the pin "SPK+" and "SPK-" outputs the sound.
Score Counter (7-segment LED Display)
I used two cathode common 7-segment LED for displaying the current score and high-score. What I chose is very tiny but already discontinued now, so you need to find an alternative.
You may need to utilize an LED driver IC when you implement a 7-segment LED because the circuit is required to drive many LEDs. In this project, we selected TM1640, which can control up to 16 LEDs and has reasonable price.
When you use it, you need to connect the pin 9 - 16 (SEG1 - 8) to the anode pins (A - G, DP) of the both 7-segment LEDs, and the pin 18 - 25 (GRID1 - 8) to each cathode pin of them. Notice the order of the GRID pins is descending, which means you have to connect the GRID 1 to the most significant digit of the LED and GRID 4 to the least one of the high-score display, and same as the GRID 5 - 8 and current score display.
TM1640 has a serial interface to receive operations to control LEDs from an MCU. I have connected DIN to digital pin 8 and SCLK to pin 7 of Arduino.
Push Buttons With a Single I/O Pin
Then, I also soldered 5 small push buttons (tactile switches) on the board. All buttons are connected in parallel to the same analog input pin (A0 in this project). This system can utilize multiple buttons with a single input pin but detect only one is pressed even if you press multiple buttons simultaneously.
My system uses a voltage divider, a circuit of a series of resistors, to detect which button is pressed. This circuit divides the applied voltage to the input pin into 6 levels from 0V to the supply voltage (Vcc). You might have already found the value of all resistors are completely different in each other because it's convenient that the gap of each voltage level is equivalent and I had to calculate the proper resistance values. According to this article, I needed to prepare one 2.2k, 4.7k, 5.6k, and 33kΩ resistor suppose that treating a 10kΩ resistor as the pull-up resistor.
To implement this circuit, first you connect 10kΩ, 2.2kΩ, 4.7k, 5.6k, and 33kΩ serially in this order. Next, you attach one side of each button to the all of the middle of resistors or the edge of the 33kΩ. You also connect all the other side of the buttons to the GND and finally do A0 pin between the resistor 10kΩ and 2.2kΩ.
Function of each button is defined as SELECT, UP, DOWN, LEFT, and RIGHT in order near from the upper voltage side.
WS2812B (matrix Display & Illumination)
This is the most important component for my game device.
The main screen having 16 x 8 resolutions is composed of two WS2812-2020 matrix displays. This module has 8 x 8 resolutions and links whole LEDs serially inside the panel. Soldering a connector behind the both matrix LED modules and plugging them to link together and they work as one 16 x 8 resolution display. I connected the pin D9 (Arduino) to the display module.
I added 2 more WS2812B module to make the game and its player more excited with lighting effects like colors or animations. The input pins of the both LEDs are connected to the same pin D6 (Arduino).
These modules consume are supplied their electricity from the power source (battery) because of their highly consumption.
Install Necessary Libraries
Now you have understood all of the electronic components I used.
What you learn next is the required libraries to develop the firmware for Arduino. You may have already installed Arduino IDE, but you can download the installer or software package here if not.
After installing the IDE, you also have to install the following libraries.
- Adafruit NeoPixel
- TM16xx
- Adafruit GFX
- Adafruit BusIO
Except the 2nd item, you can install them from "library manager" accessing from the tab menu "sketch". But, don't worry, you can get the zip file from Github, and execute "Add .ZIP Library" in the menu Sketch > Include Library. TM16xx library uses Adafruit GFX and BusIO library internally, so that's why we need to install 2 extra libraries.
Uploading Software
Here's the main source of the firmware. Just copy and paste it on a new file of Arduino IDE. But wait a moment to upload it. Even if you press the upload button, you will see an error causes.
// https://create.arduino.cc/projecthub/john-bradnam/wii-chuck-neopixel-tetris-game-047fdc // http://electronoobs.com/eng_arduino_tut104.php #include <Adafruit_NeoPixel.h> #ifdef __AVR__ #include <avr/power.h> // Required for 16 MHz Adafruit Trinket #endif #include <EEPROM.h> #include <TM1640.h> // requiring install Adafruit GFX and BusIO library #include <DFRobotDFPlayerMini.h> #include <SoftwareSerial.h> #include "PROGMEM_readAnything.h" // game FPS #define FRAMERATE 30 // initial speed of falling (frames) #define INIT_SPEED 20 // animation speed (frames) #define FRAME_ANIME 5 // deal with that a pixel is blank #define EMPTY 16 // number of columns #define MATRIX_COLS 8 // number of rows #define MATRIX_ROWS 16 // brightness of the display #define MATRIX_BRIGHT 4 // 0 ~ 16 // color when removing line #define MATRIX_REMOVE 0x888888 // number of buttons #define NUM_BUTTON 5 // button pattern #define BUTTON_SELECT 0 #define BUTTON_UP 1 #define BUTTON_DOWN 3 #define BUTTON_LEFT 2 #define BUTTON_RIGHT 4 // speaker volume #define SPK_VOLUME 20 // 0 ~ 30 // number of the track #define NUM_BGM_MAIN 1 #define NUM_SE_TITLE 3 #define NUM_SE_GAMEOVER 4 // EEPROM address for high score #define ADDR_EEPROM_SCORE_UPPER 1 #define ADDR_EEPROM_SCORE_LOWER 0 /** * constants */ typedef struct { byte id; byte width; byte height; bool picture[6]; byte turns; // number of possible turns byte cx; byte cy; uint32_t color; } Piece; // features of puzzle pieces const uint32_t pieces_color[7] = {0xFF00FF, 0x009900, 0xFF8000, 0x00FFFF, 0x0000FF, 0xFF0000, 0xFFFF00}; const Piece pieces[7] PROGMEM = { {0, 3, 2, {0, 1, 0, 1, 1, 1}, 4, 1, 1, pieces_color[0]}, //T:purple {1, 3, 2, {0, 1, 1, 1, 1, 0}, 2, 1, 0, pieces_color[1]}, //S:green {2, 2, 3, {1, 0, 1, 0, 1, 1}, 4, 0, 1, pieces_color[2]}, //L:orange {3, 4, 1, {1, 1, 1, 1, 0, 0}, 2, 2, 0, pieces_color[3]}, //I:cyan {4, 2, 3, {0, 1, 0, 1, 1, 1}, 4, 1, 1, pieces_color[4]}, //J:blue {5, 3, 2, {1, 1, 0, 0, 1, 1}, 2, 1, 0, pieces_color[5]}, //Z:red {6, 2, 2, {1, 1, 1, 1, 0, 0}, 1, 0, 0, pieces_color[6]} //O:yellow }; const int num_pieces = sizeof(pieces_color) / sizeof(pieces_color[0]); // configuration of button input pin // 0, 182, 418, 569, 840, 1023 const int thres_step = (1 << 10) / NUM_BUTTON; // step voltage of each button state const int thres_bias = 50; // deviation from the average step /** * globals */ // game state and condition byte gamestate = 0; //0: title, 1: main game, 2: result int speed = INIT_SPEED; //lower one every 10 frames (smaller = faster) // game board // buffer to store which block is filled in each pixel byte board[MATRIX_COLS * MATRIX_ROWS]; // game score unsigned int score = 0; unsigned int highscore = 0; int lines = 0; // current block to use Piece piece; // rotation byte rotation = 0; //0, 1, 2, 3 // horizontal and vertical position int xpos, ypos; // LED Matrix Adafruit_NeoPixel matrix = Adafruit_NeoPixel(MATRIX_COLS * MATRIX_ROWS, 6, NEO_GRB + NEO_KHZ800); // illumination LED Adafruit_NeoPixel deco = Adafruit_NeoPixel(1, 9, NEO_GRB + NEO_KHZ800); // score counter // TM1640 (Data in: 7, Clock: 8, 8 digits) TM1640 module(7, 8, 8); // MP3 player DFRobotDFPlayerMini mp3; // software serial for DF player. RX: 5, TX: 4 SoftwareSerial mp3_serial(5, 4); void setup() { // put your setup code here, to run once: // These lines are specifically to support the Adafruit Trinket 5V 16 MHz. // Any other board, you can remove this part (but no harm leaving it): #if defined(__AVR_ATtiny85__) && (F_CPU == 16000000) clock_prescale_set(clock_div_1); #endif // serial communication setup Serial.begin(9600); mp3_serial.begin(9600); // random seed initialization randomSeed(micros()); //millis() // load the high score highscore = ((unsigned int)EEPROM.read(ADDR_EEPROM_SCORE_UPPER) << 8) + (EEPROM.read(ADDR_EEPROM_SCORE_LOWER)); // speaker if(!mp3.begin(mp3_serial)){ Serial.println(F("Unable to begin:")); Serial.println(F("1.Please recheck the connection!")); Serial.println(F("2.Please insert the SD card!")); while(true){ delay(0); // Code to compatible with ESP8266 watch dog. } } // speaker configuration mp3.volume(SPK_VOLUME); // starting sound mp3.play(NUM_SE_TITLE); // illumination LED deco.begin(); deco.show(); // LED matrix matrix.begin(); matrix.setBrightness(MATRIX_BRIGHT); // all pixels to 'off' matrix.show(); } void loop() { // put your main code here, to run repeatedly: static unsigned long frameCount = 0; static bool pre_pressed[NUM_BUTTON] = {false, false, false, false, false}; // detect pressed button // multiple buttons on single Analog Pin (A0) bool pressed[NUM_BUTTON] = {false, false, false, false, false}; // applied voltage to the button pin int val = analogRead(A0); // detection process for(int i = 0; i < NUM_BUTTON; i++){ // check pressed if the value of AD conversion bool state = (val > (thres_step * i - thres_bias)) && (val < (thres_step * i + thres_bias)); // only the down button is acceptable for holding down if(i == BUTTON_DOWN){ pressed[i] = state; } else{ // reproduce pseudo rising detection pressed[i] = !pre_pressed[i] && state; // store the current state of the button pre_pressed[i] = state; } } // title if(gamestate == 0){ // game logo module.setDisplayToString("TOCHORIS"); // start if the select button pressed if(pressed[BUTTON_SELECT]){ // initialize game resetGame(); // main game BGM mp3.play(NUM_BGM_MAIN); // go to the main game gamestate = 1; } // title scene if(frameCount % FRAME_ANIME == 0){ // rainbow illumination as the uint16_t hue = (uint16_t)(millis() % 65536UL); deco.setPixelColor(0, deco.gamma32(deco.ColorHSV(hue))); deco.show(); // LED matrix display // for(int y = 0; y < MATRIX_ROWS; y++){ // for(int x = 0; x < MATRIX_COLS; x++){ // int index = x + y * MATRIX_COLS; // uint16_t col = (uint16_t)((millis() + 4096UL * index / matrix.numPixels()) % 65536UL); // matrix.setPixelColor(index, matrix.gamma32(matrix.ColorHSV(col))); // } // } // pick-up random color of the pieces for(int i = 0; i < matrix.numPixels(); i++){ matrix.setPixelColor(i, pieces_color[random(0, num_pieces)]); } } } // main game else if(gamestate == 1){ // drawing LED matrix setBoardPixel(); // control process if(pressed[BUTTON_LEFT]){ if(checkMovable(piece, xpos - 1, ypos, rotation)){ xpos--; } } if(pressed[BUTTON_RIGHT]){ if(checkMovable(piece, xpos + 1, ypos, rotation)){ xpos++; } } if(pressed[BUTTON_DOWN]){ if(checkMovable(piece, xpos, ypos + 1, rotation)){ ypos++; } } if(pressed[BUTTON_UP]){ int nrot = (rotation + 1) % piece.turns; if(checkMovable(piece, xpos, ypos, nrot)){ rotation = nrot; } } // add latest pose of the piece on board overwritePiece(piece, xpos, ypos, rotation); // lowing blocks if(frameCount % speed == 0){ // fall down if there is a space if(checkMovable(piece, xpos, ypos + 1, rotation)){ ypos++; } // piece on the ground else{ // paint game board storePieceOnBoard(piece, xpos, ypos, rotation); // check lines checkBoardLines(); // take out new piece generatePiece(); // LED matrix matrix.show(); // check whether game is over if(!checkMovable(piece, xpos, ypos, rotation)){ // GAMEOVER gamestate = 2; storePieceOnBoard(piece, xpos, ypos, rotation); overwritePiece(piece, xpos, ypos, rotation); // Gameover sound mp3.play(NUM_SE_GAMEOVER); } } } // illumination LED indicates the piece color deco.setPixelColor(0, piece.color); deco.show(); // indicate the score displayScore(score, 7, 4); // indicate the high score displayScore(highscore, 3, 4); } // result (game over) else if(gamestate == 2){ // when select button pressed if(pressed[BUTTON_SELECT]){ // update high score if(score > highscore){ highscore = score; // save the high score EEPROM.write(ADDR_EEPROM_SCORE_LOWER, (byte)(highscore & 0xFF)); EEPROM.write(ADDR_EEPROM_SCORE_UPPER, (byte)((highscore >> 8) & 0xFF)); } // clean all matrix matrix.clear(); // go to the title scene gamestate = 0; // title music mp3.play(NUM_SE_TITLE); } if(frameCount % FRAME_ANIME == 0){ // new record if(score > highscore){ // rainbow illumination deco.setPixelColor(0, pieces_color[random(0, num_pieces)]); deco.show(); } } } // Matrix LED flash matrix.show(); // update frame and waiting frameCount++; delay(1000 / FRAMERATE); } // find where each part of a block is in the board int calculatePieceIndex(int x, int y, int dx, int dy, byte rot){ // 4 rotational patterns // define the top and right as positive direction switch(rot){ case 0: // deg 0 return (y + dy) * MATRIX_COLS + x + dx; case 1: // deg 90 return (y + dx) * MATRIX_COLS + x - dy; case 2: // deg 180 return (y - dy) * MATRIX_COLS + x - dx; case 3: // deg 270 return (y - dx) * MATRIX_COLS + x + dy; default: return -1; } } // remapping the index of the board to that of the LED matrix int remapBoardToLEDpixel(int pos){ // mapping as inverted position return matrix.numPixels() - 1 - pos; // index of the board buffer is same as that of LED matrix // return pos; } // generate next piece void generatePiece(){ // select a piece PROGMEM_readAnything(&pieces[random(0, num_pieces)], piece); // initialize the position xpos = MATRIX_COLS / 2; ypos = piece.cy; rotation = 0; } //whether a block can move somewhere bool checkMovable(Piece piece, int x, int y, byte rot){ // return true if it can fall, false if not for(int i = 0; i < piece.width; i++){ for(int j = 0; j < piece.height; j++){ if(piece.picture[i + j * piece.width]){ int xx, yy; switch(rot){ case 0: xx = x + i - piece.cx; yy = y + j - piece.cy; break; case 1: xx = x - j + piece.cy; yy = y + i - piece.cx; break; case 2: xx = x - i + piece.cx; yy = y - j + piece.cy; break; case 3: xx = x + j - piece.cy; yy = y - i + piece.cx; break; } if(xx < 0 || xx >= MATRIX_COLS || yy < 0 || yy >= MATRIX_ROWS) return false; if(board[xx + yy * MATRIX_COLS] != EMPTY) return false; } } } return true; } // clean full board line void checkBoardLines(){ // number of lines deleted at once. int account = 0; // scanning each rows up to down for(int y = 0; y < MATRIX_ROWS; y++){ bool complete = true; // check all pixels in a single line are filled for(int x = 0; x < MATRIX_COLS; x++){ if(board[x + y * MATRIX_COLS] == EMPTY) complete = false; } // if a line is filled with blocks if(complete){ account++; lines++; // LED animation of removing line removeLine(y); // lowing blocks for(int yy = y; yy >= 0; yy--){ for(int xx = 0; xx < MATRIX_COLS; xx++){ // top line will always be empty if(yy == 0){ board[xx] = EMPTY; } // otherwise block falls to the below pixel (substitute the above pixel) else{ int i = xx + yy * MATRIX_COLS; board[i] = board[i - MATRIX_COLS]; } } } // update and show display setBoardPixel(); matrix.show(); } } // there is a line deleted if(account > 0){ // add score point // corresponding to the number of deleting line: 10 * x * (x + 1) score += 5 * account * (account + 1); // 10, 30, 60, 100 // update game speed (every 10 lines) speed = max(0, INIT_SPEED - floor(lines / 10)); } } // paint the piece in the LED matrix void overwritePiece(Piece piece, int x, int y, byte rot){ for(int i = 0; i < piece.width; i++){ for(int j = 0; j < piece.height; j++){ if(piece.picture[i + j * piece.width]){ int pos = calculatePieceIndex(x, y, i - piece.cx, j - piece.cy, rot); // set as position inverted matrix.setPixelColor(remapBoardToLEDpixel(pos), piece.color); } } } } // paint the piece on the board (in memory buffer) void storePieceOnBoard(Piece piece, int x, int y, byte rot){ for(int i = 0; i < piece.width; i++){ for(int j = 0; j < piece.height; j++){ if(piece.picture[i + j * piece.width]){ int pos = calculatePieceIndex(x, y, i - piece.cx, j - piece.cy, rot); board[pos] = piece.id; } } } } // paint the board in the LED matrix void setBoardPixel(){ for(int i = 0; i < matrix.numPixels(); i++){ // fill the pixel color with the chosen block color if(board[i] != EMPTY){ matrix.setPixelColor(remapBoardToLEDpixel(i), pieces_color[board[i]]); } else{ matrix.setPixelColor(remapBoardToLEDpixel(i), 0); } } } // animation deleting line void removeLine(int y){ for(int x = 0; x < MATRIX_COLS; x++){ //flash LED int index = remapBoardToLEDpixel(x + y * MATRIX_COLS); matrix.setPixelColor(index, MATRIX_REMOVE); matrix.show(); delay(50); matrix.setPixelColor(index, 0); } } // fill with EMPTY void cleanBoard(){ for(int i = 0; i < matrix.numPixels(); i++){ board[i] = EMPTY; } } // initialize game condition void resetGame(){ // reset game score and speed lines = score = 0; speed = INIT_SPEED; // make all pixels empty cleanBoard(); // select the first piece generatePiece(); } // display a score with 7 segments LED // [arg] score: score number, place: index of the ones place, digits: number of displays void displayScore(int score, int place, int digits){ // player score for(int i = 0; i < digits; i++){ // if the digits of the score is more than the display size, score board displays 9999 // otherwise, calculating the number in the ones place int val = score < pow(10, digits) ? score % 10 : 9; module.setDisplayDigit(val, place - i, false); // round down the ones place score /= 10; } } <br>
After you copy and paste the above code, once you add a new tab by pressing Ctrl + Shift + N or selecting "Add New Tab" in a small button with upside-down triangle, then copy and paste the following script, and finally save it with naming it "PROGMEM_readAnything.h".
//https://arduino.stackexchange.com/questions/13545/using-progmem-to-store-array-of-structs #include <Arduino.h> template <typename T> void PROGMEM_readAnything (const T* sce, T& dest){ memcpy_P(&dest, sce, sizeof(T)); } template <typename T> T PROGMEM_getAnything (const T* sce){ static T temp; memcpy_P(&temp, sce, sizeof(T)); return temp; }<br>
Now you are ready to upload the firmware. Just pressing the upload button.
Creating Its Chassis
Nothing to say about its housing. Basically you can use any materials you prefer like paper or cardboard, but I created it with 3D printer. I bought X-maker provided by QIDI TECH, but instead you can choose different models like that because what l bought is out of stock now.
There are 3 reasons to recommend you to use 3D printer, 1) it provides you higher flexibility of cases, 2) usually you will get a durable and good-looking one compared to paper crafts, 3) you can store and share it as 3D model data. If you start using a 3D printer, you should choose material to ABS or PLA because of their ease of handling.
In my case, I have printed out the center plate and 2 side steeples individually and combine them. The steeples are separated into the upper and lower side. The matrix LED displays are attached behind the center plate, and in each tower the pair of 7-segment LED and decoration full-color LED are installed. When you install them, you need to put the 7-segment LED in the rectangle hole and decoration LED on the top of the lower side of the tower.
These attached 4 STL files are the data I used to create its housing. Unfortunately, the current ones are not still the complete version because these don't have screw holes to be assembled into the miniature "Tocho". You need to make screw holes or use a glue to fix all of the components (I chose the former way). But feel free to utilize these STL files even if you can accept to process the housing parts.
Assemble
Now all components must be ready.
The left picture shows how the circuit is assembled and everything would consist of the device like the right picture. I use 3 AAA batteries for the power supply.
When you install 7-segment LED and decoration full-color LED into the steeples, you need to put the 7-segment LED in the rectangle hole and decoration LED on the top of the lower side of the tower.
How to Play
Got it! After finishing all steps, you will see the complete one.
Now let's enjoy "TOCHORIS"! When turning on its power, you can see the opening effect of the LEDs.
You press the SELECT button to start the game, and its rule is almost the same as TETRIS, so you strategically rotate, move, and drop blocks falling into the rectangular field to clear as many lines as possible by completing horizontal rows of blocks without empty space. More lines you deletes, more scores you will get.
If the block reaches to the top, the game is over. You can see an extra effect (rainbow illumination) if you break the high score.