/*/////////////////////////////////////////////////////////////////////////////////////// * * ____ /\\ * // __\ __ _ _ _ _ _ _ ___ / // * // / / \ | | | | | | | | | | | | | / / // * // / / /\ \ | | | | | | | \ / | | \ | | / // * // / / / \ \ | | | | | | | \ / | | \| | \ \\ * \\ \ \ \ / / | | | | | | | \/ | | |\ | \ \\ * \\ \ \ \/ / _| |___ | |__| | | |\ /| |_| | \ | \ \\ * \\ \__-| \__/ |_____/ \______/ |_| \/ |___|_| \_| / // * \\____/ /__// * for Arduino * * by K4ICY * * * Based on the Arcade and Home Console Video Game "Columns" - ©1990 Sega Corporation * Original concept by: Jay Geertsen (1989) * * A Match-Three Puzzle Game https://en.wikipedia.org/wiki/Columns_(video_game) * * by Michael A. Maynard, a.k.a. "K4ICY" Visit: http://www.k4icy.com/ * * Arduino IDE Code/Sketch and Game Implementation, Copyright ©2019 - 2022 * * CC BY-NC-SA 3.0 * * Code is Open Source and free to alter and distribute. * If copying or using derivitive of this code, * please give proper credit to the original author/programmer. * Not for Commercial Use. * * Version 08e.1 2022/27/02 For the Mark II SMD/PCB Platform * * Questions and Correspondence: mikek4icy@gmail.com * */////////////////////////////////////////////////////////////////////////////////////// /* * * There are FOUR games included! * * Game Play: ORIGINAL COLUMNS * * The area of play is a tall grid, like a well where 'columns' of three * randomly-colored jewels appear, one at a time, from the top of the well * which fall to the bottom landing either on the floor or on top of * previously-fallen columns. While a play piece column is falling, the player * can position it horizontally using the [LEFT] and [RIGHT] buttons. The order * of the jewel types (colors) within the column can be shifted from top to bottom * by pressing the Up [ARRANGE] button. The rate at which the column falls can * be increased by pressing or holding the [DROP] button and slowed by holding * the [START] (Slow) button. After the column lands, if matching sets of three or * more jewels of the same color are found to be arranged together within the play * field in either horizontal, vertical, or diagonal lines, those jewels will be * 'crushed' and will disappear. The pile of remaining jewels then settles * under gravity. If this resettlement causes more jewels types to be aligned, * then they too disappear and the chain reaction cycle repeats. Occasionally, * a very special column with brilliant multicolor Magic Jewels appears. When it * lands, all the jewels in the entire play field that are the same color as the * one it lands on will be removed! The columns fall at a faster rate as the player * progresses through higher levels which increase according to the number of * jewels collected. * * The goal of Original Columns is to play for as long as possible and collect as * many jewels as can be done before the well fills up. When any column settles * above the top of the play field, it's GAME OVER. Players can score up to * 9,9999,999 points. * * Game Play: FLASH COLUMNS * * The goal of Flash Columns is to mine your way through a user-set number of * rows filled with jewels on the play field to get to a flashing jewel at the bottom. * In lieu of a score, the time (in minutes and seconds) is kept and players are * encouraged to crush the flashing jewel in the fastest amount of time. Flash Columns * can end one of two ways, either good or bad. If the jewels reach the top then the * player's time is discarded. * * The same general rules apply to Flash Columns as with Original Columns except the * Magic Jewel will not appear to help the player and a good bit of strategy has to be * used to dig through a preset solid stack of randomly placed jewels. * * Game Play: TIME TRIAL * * This is Original Columns except the player is racing against a 3-minute countdown * timer and the goal is to earn the highest score. The use of the DROP feature as * well as the player starting off at a higher level will have advantages for scoring. * There are no Magic Jewels available. Both Flash Collumns and Time Trial are * native to the Sega console version of Columns. * * Game Play: CRUSH COLUMNS * * This is also Original Columns, but with a twist! The player will find the floor * of the Play Field literally raising as rows of the dreaded 'Crush Bar' are occasionally * added. If the Crush Bar pushes any lying jewels over the top it's game over! The * Crush Bar can be lowered a row at a time by collecting a certain number of jewels within * each landing cycle, which isn't going to always be easy, but it can be lowered by much * more when the Magic Jewel lands, which is readily available at multiple times on any * level. Even better still, the Magic Jewel, if made to DROP with enough distance, can * eliminate the majority of the Crush Bar. But wait, there's more! A very helpful * randomly placed Flashing Jewel, if crushed, will cause the entire Crush Bar to be * eliminated! There are one or two other goodies that the player will just have to * discover and the potential for scoring is much greater. Be warned, Crush Columns is * a much more chaotic and harder to beat game than Original Columns and the Crush Bar is * not to be underestimated. In fact, it seems to prey and pounce. Crush Columns was * inspired by the Sega Collumns III arcade and console game and is a fun addition! * * Some Play Mechanics: * * The Play Field grid, determined by xField for height (columns) and yField for width (rows), * typically 16 high x 6 wide with the visible portion, which is three row less than xField * is displayed by a matrix of addressable RGB LEDs. The top three rows are shrouded as * part of the game play. Some varieties of the Columns-type game treat the specifics * of the top rows interaction and game over conditions differently. These rows can be * thought of as a 'staging area' or 'soft zone' which is unique to the K4ICY design. * The player can still position their "column" (containing three jewels) in this zone * which may be used to the players advantage, since a match may be found in a saving last * ditch effort but any jewels remaining in this area will cause the game to end. The * "columns" (play pieces) march down the Play Field by 'gravity', starting at 1 cell per * second, accelerating a small amount as it progresses towards the floor. Game Levels * are increased according to the number of jewels smashed, typically 25 each, and a * logistic decay curve is applied to the speed increase / per-level which is further increased * by playing at a harder difficulty setting. In the K4ICY version of Columns (Original * Columns), scoring is typically determined as follows: gameScore += (Jewels Smashed x * ((Level + 1) x 10 x Number of Chain Reactions)) and additional points given when the column * is pushed downwards (DROP) by user: gameScore += (((Cell Steps Down x 2) - 1) x (Level + 1)). * Additional bonus points may be given for other factors including crushing jewel strings * longer than three. The other game variant including Flash Columns, Time Trial and Crush * Columns have additional multipliers due to their "hot" play dynamic. Five buttons are * available: [START]/Slow, [LEFT], [RIGHT], [ARRANGE] and [DROP]. * * Tapping [LEFT] and [RIGHT] will move the descending column latterally (restricted by the edges * and other pieces) and holding the buttons will perform a /Short Delay Auto-Shift/. There is NO * pause option on purpose but can be easily added by the builder if desired. [START]/(Slow) will * slow down the descent by roughly 1/2 and [DROP] will employ a rapid descent rate of 1/35. Speeds * have lower limits at higher levels due to limitations of maximum AVR/sketch timing rates. * * Menu Mode: * * This is the default operation mode when the unit is first powered and when a game is not * playing. The high score for each particular game, best times, previous score and times, * the currently set game title mode and other pertinent information, such as a charge * battery charge warning, will cycle through on the 7-segment numerical LED displays. * * Pressing the [LEFT] or [RIGHT] buttons will change the game mode between Original Columns, * Flash Columns, Time Trial Columns and Crush Columns, and pressing both at once will cycle * through the LED brightness levels (16) which will be saved once a game is started. * * Pressing the [ARRANGE] or [DROP] buttons will change the Audio Volume levels. Pressing both * buttons at the same time will turn the rumble motors on or off. These settings will also * be saved once a game is started. * * The [START]/(Slow) button will begin a selected game and will first bring up the Pre-Game Menu. * The Original Columns, Time Trial Columns and Crush Columns game modes will first let you * choose the Difficulty (number of jewels colors in play: 3 - 8) by pressing the [LEFT] or [RIGHT] * buttons and the Class (speed increase pace: 1 - 7) by pressing the [ARRANGE] and [DROP] buttons. * The Flash Columns pre-game menu will let you set the Difficulty with [LEFT] and [RIGHT] and the * Height (number of pre-filled rows: 2 - 10) is selected with the [ARRANGE] and [DROP] buttons. * The new game will begin to play once the [START]/(Slow) button is pressed again or after a 20 * seconds wait without user input. * * Sketch Characteristics: * * There are no Interupt features use for button capturing, event timing and etc. Instead, a form * of 'meta-timing' is used whereas timing event markers are set and functions are executed once * those future timing markers have been passed. Timing is therefore less-accurate but this leaves * a lot of flex room for operation 'slop'. Major operation event modes for the game are broken * down into nested loops and if/while conditions along with actual "goto" actions. There's also * an inordinate number of "magic" numbers used and many redundant operations have been relegated * to functions to help mitigate SRAM space. A lot of ugly and agregious programming tactics which * make debugging impossible to follow... yes, but damn, was there a lot of game crammed into an * entry-level AVR! * */////////////////////////////////////////////////////////////////////////////////////// /* * Setup Notes: * * Addressable Devices * - (2) Max7219 7-Segment (x8 digit) LED modules, connected in series * - (2) FDPlayer Mini MP3 modules (DFRobot) with SD card containing sound effects and music * care must be taken to merge the two audio outputs to one set of speaker: from the DAC outputs through resistors/cap networks, etc. * - Display, consisting of addressable LEDs compatible with the FastLED library (ie. WS2811, WS2812B, NeoPixel, etc.) * should have play field dimensions of at least 6x by 5y, and up to the memory capacity of uC device (requires experimentation) * Data line is wired in "zig-zag" and not "serpentine" unless you reconfigure the "ledindex[]" for an appropriate mapping loop * A few additional LEDs could be used for light-up play buttons (within bounds of memory overhead) * Typical Devices * - Rumble Motors; Small, Large - as used in standard 360 game controllers, or use smaller form factor. MOSFET control of Rumble Motors * - Audio mixing circuitry and high-efficiency audio amplifier module - will require EDC/RF noise mitigation * Muting scheme (there is a mute pin on the amp IC) should be implemented for first half-second of startup to eliminate in-rush squealing * - DC-DC Boost Converter recommended for self-contained dev-board microcontrollers, such as UnoR3 and Nano if 5v is the main supply, due to LDO voltage drop on that device's regulator * - 3.8 - 4.5 vDC from adjustable voltage regulator recommended for MP3 modules * Sketch - #define NUM_LEDS = grid size plus 3 (used for preview) * - const int xField (and yField) designate grid size of the game's logical Play Field, xField - 3 determines physical LED Play Field display height dimension * Reset / Device Issues * - A "Factory Reset" can be performed by holding down Left+Right+Up+Down during Power-On. All other settings set to 0 which will all have to be set, playing at least one game to write them to EEPROM * - Look for other remedies (commented out) in the Void Setup() section for addressing EEPROM memory issues or for pre-setting high scores * Current/Power - Test (6 x 13 matrix +3 of WS2812B): 1900 ma at possible maximum game play with possible 2400 ma max! * Setting default is restricted and the builder should set power maxes in consideration to power supply and etc. * 200-450 ma tested at idle with bursts of ~700 ma, and play giving 3-hours plus play on (1300 mWh x 2) 1s2p external 18650 cell Li-Ion power source * Boost and Buck converters may likely not be required. A small adjustable LDO regulator, set to around 4.2v should be used for the DFPlayers as 5v is their absolute max. * The Arduino dev modules including the UnoR3 and Nano will require more than 5 volts (7.5 min) due to thier AVR uC IC's being run from their own LDO voltage regulator. * Additional * ISOLATE POWER SOURCE to AVR uC IC from other game devices when programming! * Or provide a separate 5v power source to AVR, making sure to bond the GND lines. * The AVR should only be powered (alone) by programming power sources * On the Arduino Uno, Pin's 1 and 0 are unused, but provided that they're not connected to * anything at the time of programming, they could be used for other features. Pins 19 and 22 * on the Arduino Nano are analog only, but sould be used for the battery voltage reading. Any additional analog pins (A7) could be used for something like a photo-resistor * to automatically adjust the LED brightness on a portable unit. * When setting up a 'bare bones' uC on a custom PCB (ATmega328P-xx) the pins used for both ICSP (bootloader) and FTDI (programming) * may need to be left unused or switchable via header jumpers, or using an op-amp buffer to avoid obvious conflict. * The ICSP port should be the primary programming method as the bootloader will be omitted which may leave the only allowance of space for this sketch. * The FTDI port requires a bootloader to be first installed via the ICSP port. * State of Charge for Portable Battery * The state of charge (SOC) reading/decoding on any portable battery source is generally a difficult task. * A resistor divider network comprised of two 1M ohm resistors feeds ~(1/2 battery voltage) to Analog pin 5. * Using any value over 10k isn't recommended for the AVR ADC inputs due larger impedances hindering the function of the internal charge capacitors on the ADC. * The caps take longer to charge/discharge producing unpredictable readings. Enough time passes between each analog read even to apparently deal with this to a degree, * at least on my boards. A 10nF cap can be paralleled on the the A5-to-Gnd resistor might be worth a try if you're having trouble. * 1M resistors were chosen to reduce battery drain, especially when the game is not running. * A more relaiable 10k/10k net can be used by reading the battery (+) line in conjunction with a blocking diode or power path control mosfet if you have access to the BMS circuit. * Current drain is heavy on a game using bright LEDs, so at the very least, a friendly warning from the game would be nice to remind the user to charge the battery. * * CHECKLIST OF POSSIBLE HANG-UP ITEMS THAT NEED TO BE CUSTOMIZED BY USER: * 1) Addressible LED Module Type and RGB order - i.e. : FastLED.addLeds(leds, NUM_LEDS); * 2) Color Palette - byte jewelHue[] = {0, 0, 20, 55, 96, 115, 160, 225, 195, 0}; - You must experiment and find optimal values * 3) AudioMutePin and uCShutDownPin - Search for the bits of code using these and comment them out if you can't or are not using them. * 4) Everything under "/// Battery Charge Indication" - IF you are not able to configure these parameters for your own Li-ion battery settup, you should disable any applicable setting * */////////////////////////////////////////////////////////////////////////////////////// /* PIN Assignments * ================================================================================================================ * Connection: Arduino IDE 'Pin': IC Pin: ATMEGA328P-AU Notes: * Up (Arrange) Button 14 (A0) 23 Buttons contact to GND * Down (Drop) Button 15 (A1) 24 * Left Button 16 (A2) 25 * Right Button 17 (A3) 26 * Start (Slow) Button 18 (A4) 27 * Power Cuttoff Signal to BMS 19 (A5) 28 * Battery Voltage Read (A6) 19 Not Avail. on Uno * ICSP SCK 13 17 * ICSP MISO 12 16 * Audio Mute / ICSP MOSI 11 15 * Tx to DFPlayer 2 Rx (pin 2) 10 14 Music * Tx to DFPlayer 1 Rx (pin 2) 9 13 Sound Effects * LDDataPin Data on 7-Seg 8 12 * LDClockPin Clock on 7-Seg 7 11 * Rumble Motor Small 6 10 * Rumble Motor Large 5 9 * LDLoadPin Load/CS on 7-Seg 4 2 * Busy on DFPlayer 2 (pin 16) 3 1 * DATA_PIN on FASTled WS2812B 2 32 * FTDI TXO {unused} 1 31 * FTDI RXI {unused} 0 30 */ /* EEPROM Game Memory Map: (Note: Some of these settings may become vestigial due to game development) * ================================================================================================================ * Address: Variable: Default: Notes: * 0 highScore additive (Original Columns & Flash Columns) * 4 highLevel additive (Original Columns) * 6 highNumJewels additive (Original Columns) * 8 Dev1Volume 25 (soft 0-30 loud) * 9 Dev2Volume 15 (soft 0-30 loud) will be 80% of Dev1 * 10 BRIGHTNESS 170 (dim 50-255 bright) determined by LEDNumBrightness * 11 LEDNumBRIGHTNESS 7 (dim 0-15 bright) * 12 jewelTypes 6 (3-8) (no longer used) * 13 magicLevel 3 (never, Level 1-199) (no longer used) * 14 difficultyFactor 60 (easy 10-250 hard) (no longer used) * 15 magicChance 920 (750 is 1:4 chance - 950 if 1:20) (no longer used) * 17 rumbleMotor on/off * 18 bestSeconds subtractive (Flash Columns) (no longer used) * 20 highLevelFC additive (Flash Columns) (no longer used) * 22 highNumJewelsFC additive (Flash Columns) (no longer used) * 24 highScoreTT additive (Time Trial Columns) * 28 highLevelTT additive (Time Trial Columns) * 30 highNumJewelsTT additive (Time Trial Columns) * 32 highScore additive (Crush Columns) * 36 highLevel additive (Crush Columns) * 38 highNumJewels additive (Crush Columns) */ // LIBRARIES ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #include // Prototyped using Version 3.3.2 #include // Prototyped using Version 1.0.6 #include #include // https://github.com/nickgammon/SendOnlySoftwareSerial // SYSTEM VARIABLES /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// FastLED Components for Addressable LED Play Field #define NUM_LEDS 81 // Number of LEDs - 13 rows x 6 columns + 3 for the preview (there are 3 more hidden rows above play field rows in game play memory, but this number pertains to actual LEDs) #define DATA_PIN 2 // Addressable LED Data Pin byte BRIGHTNESS = 180; // Limit Brightnes and thus, Current Consumption - Brightness level on LED commands will be division of total possible (50 - 255) byte SATURATION = 255; // 255 = full vivid color, 0 = white - - - used to display base white color for the Magic Jewel CRGB leds[NUM_LEDS]; // Define the array for the WS2811 addressable LEDs const byte standardBright = 64; // This is the brightness setting for a standard game piece, which is further augmented by BRIGHTNESS. Other events of varying brightness can be scaled from this variable. /// LedControl Components for LED Numerical Displays #define LDDataPin 8 // LedControl Data Pin #define LDClockPin 7 // LedControl Clock Pin #define LDLoadPin 4 // LedControl Load/CS Pin LedControl lc=LedControl(LDDataPin,LDClockPin,LDLoadPin,2); // Data In, Clock, Load/CS, Number of modules byte LEDNumBRIGHTNESS = 8; // Set default 7-seg LED Display Intensity: 0 = dim, 15 = bright // NOTE: BRIGHTNESS for FastLED setting will now be mapped to LEDNumBRIGHTNESS for Quick Settings /// DFPlayer Mini Data Components (fixed - Using (SendOnly)SoftwareSerial to interface dual units) #define Start_Byte 0x7E #define Version_Byte 0xFF #define Command_Length 0x06 #define End_Byte 0xEF #define Acknowledge 0x00 // Returns info with command 0x41 [0x01: info, 0x00: no info] byte Dev = 1; // Player Device designation (1 or 2) #define MusicBusyPin 3 // FD Player Mini 2 - Music Busy Signal SendOnlySoftwareSerial mySerial1(9); // TX - DFPlayer Mini 1 - for Sound Effects SendOnlySoftwareSerial mySerial2(10); // TX - DFPlayer Mini 2 - for Background Music byte Dev1Volume = 22; // Volume setting: 0 - 30 for Device 1 // Since we will only have one control for both volumes collectively //byte Dev2Volume = 17; // Volume setting: 0 - 30 for Device 2 // then we we will establish the music volumne to be 80% of the effects byte Dev2Volume = 20; // Volume setting: 0 - 30 for Device 2 // then we we will establish the music volumne to be 90% of the effects //byte Dev2Volume = 22; // Volume setting: 0 - 30 for Device 2 // ..or.. we can play the music just as loud /// Auxiliary Control #define AudioMutePin 11 // A Low on this pin will shut down the PAM8403 amplifier output bool unMuteAudio = false; // 'true' will send a high - audio will be temporarily muted on startup to avoid on-rush noise #define uCShutDownPin 19 // A High on this pin will cause the toggle latch on the BMS to dissengage and power to be shut down // needed only for multiple events... bool uCShutDown = false; // 'true' will send a high - power will be immediately disrupted /// Battery Charge Indication #define BatteryVoltageRead A6 // Analog Read of LiPo/Lion Battery Read (+) 3.0 to 4.3 Volts - - - Use to indicate low battery status int batteryVoltage; // if converting ADC analog reading to voltage us 'float', if only using ADC reading, use 'int' bool batteryShow = true; // 'true' will allow display of battery status such as need-for-charge condition, State Of Charge value or Voltage in the splash screen lineup int ADVbatteryLowThreshold = 352; // this is the builder preset (10-bit) lower threshold value on the battery voltage read pin for which the battery should be recharged ASAP (See Case 12 of the Start Menu for description) bool batteryLow = false; // 'true' indicates that the ADVbatteryLowThreshold was triggered so that a warning and power saving measures can be taken /// Button Pin Assignments #define ArrangeButtonPin 14 // Up (Arrange) Button #define DownButtonPin 15 // Down (Drop) Button #define LeftButtonPin 16 // Left Button #define RightButtonPin 17 // Right Button #define StartButtonPin 18 // Start (Slow) Button /// Rumble Motor Pin Assignments and specific settings #define rumbleMotorSmallPin 6 // To Rumble Motor MOSFET Gate Resistor #define rumbleMotorLargePin 5 // To Rumble Motor MOSFET Gate Resistor bool rumbleMotor = true; // Option to allow for motor activation - Splash Screen option bool rumbleMotorSmallRequest = false; // Starts a timing watch to deactivate the small rumble motor once started bool rumbleMotorLargeRequest = false; // Starts a timing watch to deactivate the large rumble motor once started unsigned long rumbleMotorDurationFactor = 50; // To time the relative duration of most rumble motor events unsigned long rumbleMotorSmallTimeMilestone; // Time event watch for small rumble motor cutoff unsigned long rumbleMotorLargeTimeMilestone; // Time event watch for large rumble motor cutoff /// GAME VARIABLES //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Playfield const byte xField = 16; // Play Field vertical dimension which includes 3 rows at the top not shown by the LED display const byte xFieldLimit = (xField - 3) ; // This establishes the visible Play Field physical demarcation byte xFieldFloor = 0; // This establishes the lower Play Field floor, considering the encroachment of the Crush Bar when it is in play const byte yField = 6; // Play Field horizontal dimension byte playField[xField][yField]; // Holds the location of existing jewels on the Play Field grid... the top 3 rows are hidden but active in the game byte ledIndex[xFieldLimit][yField]; // Reference index of physical LED addresses (and subsequent wiring route) within the Play Field /// Game Mode byte gameMode = 0; // 0 = Original Columns, 1 = Flash Columns, 2 = Time Trials, 3 = Crush Columns byte priorGameMode = 0; // This tells the Splash Screen to show only scoring from the last game mode played regardless of what is now selected byte flashColumnsHeight = 4; // This factor is user-chosen at start of Flash Columns game. Min is 2 rows, Max is xFieldLimit - 3 byte flashingJewelXPos = 0; // Any existing jewel of the Crush Columns game can qualify. Color is not important, just whatever sits there. byte flashingJewelYPos = 0; // Any existing jewel of the Flash Columns (bottom row) or Crush Columns game can qualify. Color is not important, just whatever sits there. bool flashJewelCrushed = false; // A flag used in the game whereas the Flashing Jewel was crushed and we now need to end the game. bool timeIsUp = false; // A flag used in the Time Trial game to signify when the clock has expire as opposed to the jewels reaching the top bool crushedByFloor = false; // A flag used to end the game in the rare instance the Crush Bar has made it to the top without being flooded by any jewels byte stompDownFloor = 0; // If not 0, this number will cause the Crush Bar floor to be lowered down by the specified amount byte floorRainbowIndex = 0; // This will be shifted to give color motion through the Crush Bar floor /// Jewel Characteristics byte jewelHue[] = {0, 0, 20, 55, 96, 115, 160, 225, 195, 0}; // Palette entry 8 should be reserved for the palette color that is hardest to visually discriminate since it can be omitted during sort // Establish Jewels by Hue - Used for LED data // (Array entry #:) 0 Null, 1 Red Ruby, 2 Orange Spessartite Garnet, 3 Yellow Citrine, // 4 Green Emerald, 5 Aquamarine, 6 Blue Sapphire, 7 Pink beryl, 8 Purple Amethyst, 9 Magic Jewel // 0 and 9 are special cases - 0 denotes a null space, 9 denotes the Magic Jewel which has special circumstances // Array Hue values; position 1-8 will have thier values swapped before each game for variety byte jewelTypes = 7; // The number [+1] of jewel types in play (+1 to work with random) - - - min = 3 , max = 8 - - - (6 Jewels [+1]) or 7 is default byte playSet[3] = {1, 2, 3}; // Holds the current order and type of the three descending Jewels in play, from bottom to top byte playSetNext[3] = {4, 5, 6}; // Holds the future order and type of the next play set /// Difficulty Increase int gameSpeed = 1000; // ms - This factor coincides with gameLevel - 1000ms / (level / 2) int gameSpeedSlice = gameSpeed / 25; // Used in timing byte difficultyFactor = 60; // Exponential factor applied to Game Speed vs. Level, Suggest 60 as default, 200 as arcade level, 10 requires no skill byte oldDifficultyFactor = difficultyFactor; // To revert to when a game's factor was changed in its game menu int gameSpeedDescent = gameSpeed; // This comes into play when the Down button is pressed int settleSpeed = gameSpeed * 0.65; // ms - This factor determines how long the piece will sit before becoming permanent byte playerClass = 1; // Class 1 = Level 0 to start. Class 2 = Level 15 and Class 3 = Level 30 /// Levels int gameLevel = 0; // This variable determines game speed and scoring factor, based on (numJewels-10)/25 int gameLevelStart = 0; // This variable is a temporary user-defined factor (added to gameLevel) set prior to game start as a difficulty enhancer. Will add score bonus and increased speed at startup int gameLevelPrior = 0; // Place holder for previous game level int highLevel; // High Score Game Level for Original Columns int highLevelFC; // High Score Game Level for Flash Columns int highLevelTT; // High Score Game Level for Time Trial Columns int highLevelCC; // High Score Game Level for Crush Columns /// Scoring unsigned long gameScore = 0; // Current score - increase based on jewels collected - used for all games unsigned long highScore; // (Original Columns & Flash Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons unsigned long highScoreTT; // (Time Trial Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons unsigned long highScoreCC; // (Crush Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons int Jewels = 0; // Temp total found during crush scan byte jewelTypesFound[10]; // Combo Bonus - During the crush scan, a bonus can be awarded for matching more than one type byte jewelTypesFoundTotal; // Count for different types crushed at once which can be applied as a Combo Bonus int numJewels = 0; // Total number of Jewels crushed during game int highNumJewels; // High scorer's record of number of Jewels for Original Columns int highNumJewelsFC; // High scorer's record of number of Jewels for Flash Columns int highNumJewelsTT; // High scorer's record of number of Jewels for Time Trial Columns int highNumJewelsCC; // High scorer's record of number of Jewels for Crush Columns unsigned int gameSeconds = 0; // Current seconds total for a game of Flash Columns, countdown time for Time Trial unsigned int bestSeconds = 0; // Matches current Flash Columns time total if not aleady higher, will be stored in EEPROM memory for later power-ons /// Action byte xPos; // These are the starting coordinates for the bottom playSet piece byte yPos; // byte xPos = xFieldLimit; byte yPos = (yField / 2) - 1; byte downButtonSteps; // To track how many pts to add to score for pressing Down byte crushCycle; // To boost score during Chain Reaction! byte magicTouch; // To retain attribute for what Jewel the Magic Jewel landed on byte magicLevel = 3; // Level at which the Magic Jewel may first appear - - - 0 denotes never int magicChance = 950; // Random chance setting of Magic Jewel - - - [950 of 1000 chance for not appearing] /// Music bool musicReady = true; // This flag says that a new music track can be played byte musicChoice = 28; // Place Holder byte musicPriorChoice = musicChoice; // at least don't play the last song over /// Miscellaneous Operation Flags bool settingsMenu = false; // To get into the settings mode to change game variables, reset score, vol, lighting, etc. bool bootUp = true; // To allow the sketch to get a few things out of the way when the Arduino first gets power bool gameOver = true; // To loop the game at the Splash Screen until a user wants to play bool showNext = true; // to allow the preview LEDs to show the next game piece set at the right moment bool settleFlag = false; // To know when to start the countdown timer to settle the current game piece into position bool arrangeFlag = false; // To know when the Arrange button was pressed bool leftFlag = false; // To know when the Left button was pressed bool rightFlag = false; // To know when the Right button was pressed bool arrangeButtonAllow = true; // To add debouncing to Arrange button bool leftButtonAllow = true; // To add debouncing to the Left button bool leftButtonFastAllow = false; // Signifies button hold bool rightButtonAllow = true; // To add debouncing to the Right button bool rightButtonFastAllow = false;// Signifies button hold bool downButtonFlag = false; // To handle events around Down Button activation bool startButtonFlag = false; // To handle events around Start Button activation during game used for slow-down bool upButton = false; // These are used to navigate the Settings Menu... bool downButton = false; bool leftButton = false; bool rightButton = false; bool flashColon = false; // Used to flash decimal in clock display bool bothUpDownButtons = false; bool bothLeftRightButtons = false; bool collapsePositive = false; // Used in the field collapsing procedure bool magicJewel = false; // The Magic Jewel to appear randomly from levels 3 and on. bool shelfed = false; // This flag is to keep the landing sound effect from happening more than once when landing from the side bool magicLevelAllotment = true; // This flag restricts one Magic Jewel occurance per level /// Timing Variables unsigned long timeTrack = millis(); // Will be used as a time base for many events unsigned long timePlaySet = timeTrack; // To track decent march of playSet piece unsigned long timeSettle = timeTrack; // To track any incident the piece had stopped progressing unsigned long timeArrangeButton = timeTrack; // To debounce Arrange button unsigned long timeLeftButton = timeTrack; // To debounce the Left button unsigned long timeRightButton = timeTrack; // To debounce the Right button unsigned long timeLeftButtonFastRepeat = timeTrack; // To apply timing to Left button hold fast repeat unsigned long timeRightButtonFastRepeat = timeTrack; // To apply timing to Right button hold fast repeat unsigned long timeMagicJewel = timeTrack; // To time the Magic Jewel's twinkling appearance unsigned long sparkleTiming; // Attract Mode Flashing LED's Timing unsigned long timeFlashingJewel = timeTrack; // To time the Flash Columns' flashing jewel's flashing appearance unsigned long flashColumnsSecondsTiming = timeTrack; // To time each "game second" during a Flash Columns game unsigned long gameStartReferencePoint = timeTrack; // To measure elapsed time for duration of each game (Flash Columns and Time Trial) unsigned long timeCrushBarGlitter = timeTrack; // To time the glitteration of the Crush Bar floor in Crush Columns unsigned long gameShutOffTime = timeTrack; // To send signal to shut off power (BMS) if no user input for certain duration ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - SYSTEM SETUP - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void setup() { //analogReference(DEFAULT); // Sets Analog Reference voltage to 5.0v on Uno, Nano and others accept on the Mega // (See https://www.arduino.cc/reference/en/language/functions/analog-io/analogreference/) // Sets to 3.3v on devices with 3.3v supply and etc. /// Assigned Pins /// - Change to suit device pinMode(ArrangeButtonPin, INPUT_PULLUP); // UP/ARRANGE Button pinMode(DownButtonPin, INPUT_PULLUP); // DOWN/DROP Button pinMode(LeftButtonPin, INPUT_PULLUP); // LEFT Button pinMode(RightButtonPin, INPUT_PULLUP); // RIGHT Button pinMode(StartButtonPin, INPUT_PULLUP); // START/SLOW Button pinMode(MusicBusyPin, INPUT); // To see if the DFPlayer Mini (for Music) has finished playing a track pinMode(rumbleMotorSmallPin, OUTPUT); // Assign rumble motor pins and set states to LOW which is off digitalWrite(rumbleMotorSmallPin, LOW); pinMode(rumbleMotorLargePin, OUTPUT); digitalWrite(rumbleMotorLargePin, LOW); pinMode(AudioMutePin, OUTPUT); // with a High on this pin - audio will be temporarily muted on startup to avoid on-rush noise digitalWrite(AudioMutePin, LOW); // with a Low on this pin - audio will be temporarily muted on startup to avoid on-rush noise pinMode(13, OUTPUT); // built-in LED - will be lit for a about a second to indicate that the uC is operating digitalWrite(13, HIGH); // turn on built-in LED pinMode(uCShutDownPin, OUTPUT); // with a High on this pin - the toggle latch on the BMS to dissengage and power will be shut down digitalWrite(uCShutDownPin, LOW); /// LED Numerical Displays /// EEPROM.get(11, LEDNumBRIGHTNESS); // 1 byte if (LEDNumBRIGHTNESS > 15) { // If memory is corrupt than set as a default LEDNumBRIGHTNESS = 8 ; EEPROM.put(11, LEDNumBRIGHTNESS); // 1 byte } // First Device lc.shutdown(0,false); // The MAX72xx is in power-saving mode on startup, wake up lc.setIntensity(0,LEDNumBRIGHTNESS); // Set the brightness of first display to preset value lc.clearDisplay(0); // and clear the display // Second Device lc.shutdown(1,false); // The MAX72xx is in power-saving mode on startup, wake up lc.setIntensity(1,LEDNumBRIGHTNESS); // Set the brightness of second display to preset value lc.clearDisplay(1); // and clear the display /// Addressable RGB LED Play Field /// EEPROM.get(10, BRIGHTNESS); // 1 byte if (BRIGHTNESS < 50) { // make sure that LED's will be at a minimum brightness - add a maximum brightness restriction if required BRIGHTNESS = 160 ; // If EEPROM memory is corrupt or not within range then set as a default EEPROM.put(10, BRIGHTNESS); // 1 byte } ///// WARNING!!! - Upon using a new AVR device, values stored in EEPROM will likely default to 255 (OxFF) ///// A high BRIGHTNESS level could pose a potential fire hazard or cause excessive heat damage from the LEDs. ///// The builder assumes responsibility for finding a Brightness Max level that considers safety for the user, LED modules and device housing elements. ///// Set the Bright Min level to where the addressable LEDs are reasonably dim in appearance but still retain suitable hue/color values, especially once further reduced to 25% of that. ///// A combination of issues including available current/voltage supply and even the associated math will contribute to low-brightness color instability. FastLED.addLeds(leds, NUM_LEDS); // LEDs Model, Data Pin, Color Order and Number // WS2811 is used in the PCB Prototype, WS2812B was used in the modular Prototype, change as applicable // Test the 'Color Order' before implementing. (i.e.: 'RGB' for some WS2811 5mm models and 'GRB' for the SMD WS2812B wafers) FastLED.setBrightness(BRIGHTNESS); // Sets master illumination level scale of leds FastLED.setDither(0); // Addressable LEDs can only vary the luminance of each RGB element in discrete levels of 0 to 255 so compression of the scale down with BRIGHTNESS will cause discoloration and dimming // to occur. FastLED will use 'dithering' by cycling close integer levels quickly in order to give a better visual approximation, but a drawback is, if the sketch is not rapidly updating // the LEDs (FastLED.show) fast enough to fool the eye, the LEDs appear to flicker or the colors will be off each time the LEDs are updated - so it's probably best to disable 'dithering'. int k = 0; // Fill LED Index field coordinates with physical LED addresses for (int x = 0; x < (xFieldLimit); x++) { for (int y = 0; y < yField; y++) { ledIndex[x][y] = k; // The LED Index is a map of how your LEDs are actually wired // Reprogram these x & y for-loops to suit, i.e.: serpentine, spiral, zig-zag, and cardinal orientations // x=0 is on the bottom with increments going to top // y=0 is on the right with increments going to the left // the game will use THIS ARRANGEMENT, so you must consider how to match your actual layout to this leds[k] = CRGB(0,0,0); // Also clear the play field LEDs (all off) as freshly energized LEDs may have random values k++ ; } } leds[NUM_LEDS-3] = CRGB(0,0,0); // Clear the preview LEDs as well leds[NUM_LEDS-2] = CRGB(0,0,0); leds[NUM_LEDS-1] = CRGB(0,0,0); FastLED.show(); // FastLED will now command LED modules to update states /// DFPlayer Mini (Send Only) Software Serial initialization /// - no library needed for module EEPROM.get(8, Dev1Volume); // 1 byte EEPROM.get(9, Dev2Volume); // 1 byte if (Dev1Volume > 30) { // If EEPROM memory is corrupt than set as a default Dev1Volume = 22 ; EEPROM.put(8, Dev1Volume); // 1 byte } if (Dev2Volume > 30) { // If EEPROM memory is corrupt than set as a default //Dev2Volume = 17 ; // Dev2 volume is to be 80% of Dev1's Dev2Volume = 19 ; // Dev2 volume is to be 90% of Dev1's //Dev2Volume = 22 ; // Dev2 volume is to be that of Dev1's EEPROM.put(9, Dev2Volume); // 1 byte } delay(300); // Give a bit of time for the Arduino to power up before starting DFPlayers mySerial1.begin(9600); // Sound Effects mySerial2.begin(9600); // Background Music // the [execute_CMD] function is defined at end of script in Functions section... execute_CMD(1, 0x3F, 0, 0); // initiallize DFPlayer 1 execute_CMD(2, 0x3F, 0, 0); // initiallize DFPlayer 2 delay(200); // after a bit of testing, the DFPlayer Mini's require at least 200ms to process each system control value. Music cues require less time. execute_CMD(1, 0x11,0,0); // set [no repeat] on 1 execute_CMD(2, 0x11,0,0); // set [no repeat] on 2 delay(200); execute_CMD(1, 0x06, 0, Dev1Volume); // Set the volume on 1 (0x00~0x30) execute_CMD(2, 0x06, 0, Dev2Volume); // Set the volume on 2 (0x00~0x30) delay(200); /// Saved High Score in EEPROM /// // Get High Score, High Level and High Jewel Count for Original Columns stored in EEPROM EEPROM.get(0, highScore); // 4 bytes EEPROM.get(4, highLevel); // 2 bytes EEPROM.get(6, highNumJewels); // 2 bytes if (highScore > 99999999 || highScore < 0) { // there is often junk in this EEPROM memory when using a new AVR, or the variable is stuck at some max level and this may clear it. highScore = 0; highLevel = 0; highNumJewels = 0; EEPROM.put(0, highScore); // 4 bytes EEPROM.put(4, highLevel); // 2 bytes EEPROM.put(6, highNumJewels); // 2 bytes } // Get High Score, High Level and High Jewel Count for Time Trial Columns stored in EEPROM EEPROM.get(24, highScoreTT); // 4 bytes EEPROM.get(28, highLevelTT); // 2 bytes EEPROM.get(30, highNumJewelsTT); // 2 bytes if (highScoreTT > 99999999 || highScoreTT < 0) { // there is often junk in this EEPROM memory when using a new AVR, or the variable is stuck at some max level and this may clear it. highScoreTT = 0; highLevelTT = 0; highNumJewelsTT = 0; EEPROM.put(24, highScoreTT); // 4 bytes EEPROM.put(28, highLevelTT); // 2 bytes EEPROM.put(30, highNumJewelsTT); // 2 bytes } // Get High Score, High Level and High Jewel Count for Crush Columns stored in EEPROM EEPROM.get(32, highScoreCC); // 4 bytes EEPROM.get(36, highLevelCC); // 2 bytes EEPROM.get(38, highNumJewelsCC); // 2 bytes if (highScoreCC > 99999999 || highScoreCC < 0) { // there is often junk in this EEPROM memory when using a new AVR, or the variable is stuck at some max level and this may clear it. highScoreCC = 0; highLevelCC = 0; highNumJewelsCC = 0; EEPROM.put(32, highScoreCC); // 4 bytes EEPROM.put(36, highLevelCC); // 2 bytes EEPROM.put(38, highNumJewelsCC); // 2 bytes } // Get High Score, High Level and High Jewel Count for Flash Columns stored in EEPROM /* // It was determined that the fastest time is difficult to compare due to multiple difficulty and other variables... // Best Time and associated scoring with Flash Columns will be kept and display inclusively to the game unit's current power-on state EEPROM.get(18, bestSeconds); // 2 bytes EEPROM.get(20, highLevelFC); // 2 bytes EEPROM.get(22, highNumJewelsFC); // 2 bytes if (bestSeconds > 59000 || bestSeconds < 0) { bestSeconds = 0; highLevelFC = 0; highNumJewelsFC = 0; bestSeconds = 0; highLevelFC = 0; highNumJewelsFC = 0; EEPROM.put(18, bestSeconds); // 2 bytes EEPROM.put(20, highLevelFC); // 2 bytes EEPROM.put(22, highNumJewelsFC); // 2 bytes } */ bestSeconds = 0; // If you wish to implement storing of bestSeconds for Flash Columns, comment these three lines out while uncommenting the above block highLevelFC = 0; highNumJewelsFC = 0; // Additional Saved EEPROM Game Variables /// /* // These saves were omitted after switching to an easier menu - builder can change to suit EEPROM.get(12, jewelTypes); // 1 byte if (jewelTypes > 9 || jewelTypes < 4) { // If EEPROM memory is corrupt than set as a default (6 [+1]) jewelTypes = 7 ; EEPROM.put(12, jewelTypes); // 1 byte } EEPROM.get(13, magicLevel); // 1 byte if (magicLevel > 199) { // If EEPROM memory is corrupt than set as a default (Level 3) magicLevel = 3 ; EEPROM.put(13, magicLevel); // 1 byte } EEPROM.get(14, difficultyFactor); // 1 byte if (difficultyFactor > 250 || difficultyFactor < 10) { // If EEPROM memory is corrupt than set as a default (60 Medium) difficultyFactor = 60 ; EEPROM.put(14, difficultyFactor); // 1 byte } EEPROM.get(15, magicChance); // 2 bytes if (magicChance > 1000 || magicChance < 750) { // If memory is corrupt than set as a default (920 of 1000 chance of Magic Jewel NOT appearing)) magicChance = 920 ; EEPROM.put(15, magicChance); // 2 bytes } */ /// A Little More Start Up ////////////////////////////////////////////////////////////////////////////////////// delay(200); // Let the capacitors fill first and other devices initialize... ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /* /// High Score Preset /// // Uncomment and run this portion for ONE instance if you require to preset or restore a high score: // Any saved setting, including the High Score will be wiped if uploading update sketch via ICSP port. highScore = 99999999; // Insert your numbers here ;) highLevel = 999; highNumJewels = 9999; bestSeconds = 180; EEPROM.put(0, highScore); // 4 bytes EEPROM.put(4, highLevel); // 2 bytes EEPROM.put(6, highNumJewels); // 2 bytes EEPROM.put(18, bestSeconds); // 2 bytes */ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// HIGH SCRORE RESET /// "Factory Reset" // If you have an issue with EEPROM memory corruption or Menu Mode options are not saving or you wish to wipe the high scores, // change AVR IC's or update sketch with ICSP port or hold down [LEFT] + [RIGHT] + [UP] + [DOWN] buttons upon startup... if (digitalRead(LeftButtonPin) == LOW && digitalRead(RightButtonPin) == LOW && digitalRead(ArrangeButtonPin) == LOW && digitalRead(DownButtonPin) == LOW && digitalRead(StartButtonPin) == HIGH) { for (int i = 0; i < 40; i++) { // i < ## should emcompass all required addresses EEPROM.put(i, 0x00); // Clear Bytes to 0 // or... // EEPROM.put(i, 0xFF); // Clear Bytes to 255 } while ((digitalRead(LeftButtonPin) == LOW && digitalRead(RightButtonPin) == LOW && digitalRead(ArrangeButtonPin) == LOW && digitalRead(DownButtonPin) == LOW)) {} // wait here until all buttons are depressed delay(2000); digitalWrite(uCShutDownPin, HIGH); // with a High on this pin - the toggle latch on the BMS to dissengage and power will be shut down // With a RESET, shutting the unit off will give some indication } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// digitalWrite(AudioMutePin, HIGH); // with a High on this pin - audio is now available digitalWrite(13, LOW); // turn off built-in LED - can be used later for indication purposes /// Splash Song /// // Sound Effect!!! execute_CMD(1, 0x0F, 1, 3); // Game Initiate Sound (Folder 01, File 003.mp3) on 1 - Like Coin Insert // Displays "Columns by k4icy" byte text[] = {B01001110, B00011101, B00110000, B00011100, B00010101, B00000100, B00010101, B01011011, B00011111, B00111011, B00000000, B00101111, B00110011, B00010000, B00001101, B00111011}; textOnLED(text, sizeof(text)); /// Test Rumble Motors /// EEPROM.get(17, rumbleMotor); // Rumble Motor Option (True = On / False = Off) if (rumbleMotor < 0 || rumbleMotor > 1) { // If EEPROM memory is corrupt then set as a default - data may not be boolean rumbleMotor = true ; EEPROM.put(17, rumbleMotor); // 1 byte } if (rumbleMotor) { digitalWrite(rumbleMotorSmallPin, HIGH); delay(200); digitalWrite(rumbleMotorSmallPin, LOW); //delay(200); digitalWrite(rumbleMotorLargePin, HIGH); delay(200); digitalWrite(rumbleMotorLargePin, LOW); } delay(1100); // Background Music execute_CMD(2, 0x0F, 2, 25); // Greetings Song (Folder 02, File 025.mp3) on 2 } // ...end of void setup() ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - MENU MODE - The game waits for user to start, displays scores, allows for quick settings access // // Random playfield lights flash to act as play attractor and data is repeated on LED display // This also gives time for the game to have a truly unique randomization // // - Prepare for the Splash Screen loop - pressing the [START] button will exit this loop and go to // the appropriate game setup menu // - [UP] & [DOWN] buttons will change audio volume (temporary while game is powered on) // - [LEFT] & [RIGHT] buttons will change the Game Mode - // between Original Columns and Flash Columns // - Pressing both [UP]+[DOWN] buttons together will toggle allowance for rumble motor activations // - Pressing both [LEFT]+[RIGHT] buttons together will cycle through general LED Brightness levels // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void loop() { StartGame: { /// Variables for Case Timing /// byte eventSequence = 0; // to sequence through LED data display Switch Cases unsigned long eventTargetTime = millis(); // tells the next case "frame" when it should be ran /// Variable for Game Shut Down Timing /// gameShutOffTime = millis() + 60000; // after a minute of inactivity via the buttons (in menue mode), game will be cued to shut off power - this is reset upon key press /// Variables for the Menu/Attract Mode Light Show /// sparkleTiming = millis() + 100; byte starField[xField][yField]; for (byte x = 0; x < xField; x++) { // reset star field for (byte y = 0; y < yField; y++) { starField[x][y] = 0; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - SCORE BOARD ATTRACTOR LOOP - Update the LED numerical display set at intervals to provide information ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// while (digitalRead(StartButtonPin) == HIGH && gameOver == true) { timeTrack = millis(); // time milestone reference // we will qualify the next event sequence case to be run // according to the timing specified within the case before if (timeTrack >= eventTargetTime) { // action sequences - like frames or scenes in a movie... // This Switch deals with 7-segment LED display score board action for showing game data outside of active play switch (eventSequence) { // case 1 - Displays "Columns by k4icy" // case 2 - Shows current score of previous game (all game modes,) if available // case 3 - Shows current time of previous Flash Columns game, if available // case 4 - Display "High Score OC" // case 5 - Shows high score for Original Columns, usually derived from EEPROM memory, if available // case 6 - Display "best time FC" // case 7 - Shows best time for Flash Columns, usually derived from EEPROM memory, if available // case 8 - Display "High Score TT" // case 9 - Shows high score for Time Trial, usually derived from EEPROM memory, if available // case 10 - Display "High Score CC" // case 11 - Shows high score for Crush Columns, usually derived from EEPROM memory, if available // case 12 - Display name of current game: "Original Columns", "Flash Columns", "Time Trial" or "Crush Columns" // case 13 - Display "Press Start" // case 14 - Display "RECHARGE Battery!" if battery voltage lowers below threshold // case 15 - Display "Audio" and show adjustment of Quick Volume adjustment, if active case 1: // TITLE - Show title once at startup if (bootUp == true) { // ...Text is written in setup... // Displays "Columns by k4icy" //byte text[] = {B01001110, B00011101, B00110000, B00011100, B00010101, B00000100, B00010101, B01011011, B00011111, B00111011, B00000000, B00101111, B00110011, B00010000, B00001101, B00111011}; //textOnLED(text, sizeof(text)); eventTargetTime += 4500; // add 4500 ms to current target time to move it forward in time (only when game has been first booted up) } bootUp = false; eventSequence ++; // qualify the next sequence case break; // exit the Switch case 2: // SCORE - Show current player score from previous Original Columns game - skip if there is no score (5 seconds) if ((priorGameMode == 0 || priorGameMode == 2 || priorGameMode == 3) && gameScore > 0) { // it's zero upon game unit startup, and generally not until after first game // Write the Game Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display // DISPLAY GAME SCORE - Write the last player's Game Score (non-high scorer) to the Top LED Numerical Display // scoreBoardLED(gameScore); // DISPLAY LEVEL - Write the last player's Game Level to the Left part of the Bottom Numerical Display // gameLevelLED(gameLevelPrior); // DISPLAY NUMBER OF JEWELS - Write the last player's Numeber of Jewels to the Right part of the Bottom Numerical Display // numJewelsLED(numJewels); eventTargetTime += 7000; // add 7000 ms to current target time to move it forward in time (only if showing score) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 3: // TIME - Show current player time from previous Flash Columns game - skip if there is no time to show (5 seconds) if (priorGameMode == 1 && gameSeconds > 0) { // it's zero upon Arduino startup, and generally not after first game // Write the Game Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display // DISPLAY GAME TIME - Write the last player's Game Time (non-best time) to the Top LED Numerical Display // gameTimeLED(gameSeconds, true); // DISPLAY LEVEL - Write the last player's Game Level to the Left part of the Bottom Numerical Display // gameLevelLED(gameLevelPrior); ////// ...consider keeping record specific to Flash Columns game play... // DISPLAY NUMBER OF JEWELS - Write the last player's Numeber of Jewels to the Right part of the Bottom Numerical Display // numJewelsLED(numJewels); eventTargetTime += 7000; // add 7000 ms to current target time to move it forward in time (only if showing Current Player Time) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 4: // HIGH SCORE for Original Columns - Show characters saying "High Score OC" if (highScore > 0) { // high score is generally not created until first game on Arduino is played // Displays "HIGH SCORE O.C." byte text[] = {B00000000, B00000000, B00110111, B00110000, B01011110, B00110111, B00000000, B00000000, B01011011, B01001110, B01111110, B11110111, B01001111, B00000000, B11111110, B11001110}; textOnLED(text, sizeof(text)); eventTargetTime += 2000; // add 2000 ms to current target time to move it forward in time (only if showing the text "high score oc") } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 5: // HIGH SCORE for Original Columns - Show High Score, High Level and High Jewels Number, all connected to the Original Columns High Score game played if (highScore > 0) { // high score is generally not created until first game on Arduino is played // Write the High Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display scoreBoardLED(highScore); // Write the High Score to the first 8 x 7-segment LED module // Write the Game Level Reached to the Left part of the Bottom Numerical Display gameLevelLED(highLevel); // Write the High Scorer's Game Level to the second 8 x 7-segment LED module // on the left side // Write the Number of Jewels Crushed to the Right part of the Bottom Numerical Display numJewelsLED(highNumJewels); // Write the High Scorer's Number of Jewels to the second 8 x 7-segment LED module // on the right side eventTargetTime += 4000; // add 5000 ms to current target time to move it forward in time (only if showing the high score) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 6: // BEST TIME for Flash Columns - Show characters saying "best time FC" if (bestSeconds > 0) { // Fastest Time is generally not created until first game after power-up is played and is not stored in EEPROM // Displays "best time F.C." byte text[] = {B00000000, B00000000, B00011111, B01101111, B01011011, B00001111, B00000000, B00000000, B00001111, B00000100, B00010101, B00000100, B01101111, B00000000, B11000111, B11001110}; textOnLED(text, sizeof(text)); eventTargetTime += 2000; // add 2000 ms to current target time to move it forward in time (only if showing Best Time FC) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 7: // BEST TIME - Show Fastest, High Level and High Jewels Number, all connected to the fastest Flash Columns game played if (bestSeconds > 0) { // Fastest Time is generally not created until first game after power-up is played and is not stored in EEPROM // Write the High Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display // Write the Fastest Time to the first 8 x 7-segment LED module gameTimeLED(bestSeconds, true); // Write the Game Level Reached to the Left part of the Bottom Numerical Display gameLevelLED(highLevelFC); // Write the Fastest Timer's Game Level to the second 8 x 7-segment LED module // on the left side // Write the Number of Jewels Crushed to the Right part of the Bottom Numerical Display numJewelsLED(highNumJewelsFC); // Write the Fastest Timer's Number of Jewels to the second 8 x 7-segment LED module // on the right side eventTargetTime += 4000; // add 5000 ms to current target time to move it forward in time (only if showing Fastest Time) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 8: // HIGH SCORE for Time Trial Columns - Show characters saying "High Score TT" if (highScoreTT > 0) { // high score is generally not created until first game on Arduino is played // Displays "HIGH SCORE t.t." byte text[] = {B00000000, B00000000, B00110111, B00110000, B01011110, B00110111, B00000000, B00000000, B01011011, B01001110, B01111110, B11110111, B01001111, B00000000, B10001111, B10001111}; textOnLED(text, sizeof(text)); eventTargetTime += 2000; // add 2000 ms to current target time to move it forward in time (only if showing the text "high score tt") } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 9: // HIGH SCORE for Time Trial Columns - Show High Score, High Level and High Jewels Number, all connected to the Time Trial Columns High Score game played if (highScoreTT > 0) { // high score is generally not created until first game on Arduino is played // Write the High Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display scoreBoardLED(highScoreTT); // Write the High Score to the first 8 x 7-segment LED module // Write the Game Level Reached to the Left part of the Bottom Numerical Display gameLevelLED(highLevelTT); // Write the High Scorer's Game Level to the second 8 x 7-segment LED module // on the left side // Write the Number of Jewels Crushed to the Right part of the Bottom Numerical Display numJewelsLED(highNumJewelsTT); // Write the High Scorer's Number of Jewels to the second 8 x 7-segment LED module // on the right side eventTargetTime += 4000; // add 5000 ms to current target time to move it forward in time (only if showing the high score) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 10: // HIGH SCORE for Crush Columns - Show characters saying "High Score CC" if (highScoreCC > 0) { // high score is generally not created until first game on Arduino is played // Displays "HIGH SCORE C.C." byte text[] = {B00000000, B00000000, B00110111, B00110000, B01011110, B00110111, B00000000, B00000000, B01011011, B01001110, B01111110, B11110111, B01001111, B00000000, B11001110, B11001110}; textOnLED(text, sizeof(text)); eventTargetTime += 2000; // add 2000 ms to current target time to move it forward in time (only if showing the text "high score cc") } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 11: // HIGH SCORE for Crush Columns - Show High Score, High Level and High Jewels Number, all connected to the Crush Columns High Score game played if (highScoreCC > 0) { // high score is generally not created until first game on Arduino is played // Write the High Score to the Top 7-Segment LED display lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display scoreBoardLED(highScoreCC); // Write the High Score to the first 8 x 7-segment LED module // Write the Game Level Reached to the Left part of the Bottom Numerical Display gameLevelLED(highLevelCC); // Write the High Scorer's Game Level to the second 8 x 7-segment LED module // on the left side // Write the Number of Jewels Crushed to the Right part of the Bottom Numerical Display numJewelsLED(highNumJewelsCC); // Write the High Scorer's Number of Jewels to the second 8 x 7-segment LED module // on the right side eventTargetTime += 4000; // add 5000 ms to current target time to move it forward in time (only if showing the high score) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 12: // GAME TITLE - Show characters saying the name of the current game mode if (gameMode == 0) { // Show if Original Columns Mode is selected // Displays "Original Columns" byte text[] = {B01111110, B00000101, B00010000, B01111011, B00010000, B00010101, B01111101, B00110000, B01001110, B00011101, B00110000, B00011100, B00010101, B00000100, B00010101, B01011011}; textOnLED(text, sizeof(text)); eventTargetTime += 5000; // add 4000 ms to current target time to move it forward in time (only if showing game mode changed to Original Columns) } if (gameMode == 1) { // Show if Flash Columns Mode is selected // Displays "FLASH Columns" byte text[] = {B00000000, B01000111, B00001110, B01110111, B01011011, B00110111, B00000000, B00000000, B01001110, B00011101, B00110000, B00011100, B00010101, B00000100, B00010101, B01011011}; textOnLED(text, sizeof(text)); eventTargetTime += 5000; // add 4000 ms to current target time to move it forward in time (only if showing game mode changed to Flash Columns) } if (gameMode == 2) { // Show if Time Trial Columns Mode is selected // Displays "time trial" byte text[] = {B00000000, B00001111, B00000100, B00010101, B00000100, B01101111, B00000000, B00000000, B00000000, B00001111, B00000101, B00010000, B01111101, B00110000}; textOnLED(text, sizeof(text)); eventTargetTime += 5000; // add 4000 ms to current target time to move it forward in time (only if showing game mode changed to Time Trial Columns) } if (gameMode == 3) { // Show if Crush Columns Mode is selected // Displays "Crush Columns" byte text[] = {B00000000, B01001110, B11110111, B00111110, B01011011, B00110111, B00000000, B00000000, B01001110, B00011101, B00110000, B00011100, B00010101, B00000100, B00010101, B01011011}; textOnLED(text, sizeof(text)); eventTargetTime += 5000; // add 4000 ms to current target time to move it forward in time (only if showing game mode changed to Crush Columns) } eventSequence ++; // qualify the next sequence case break; // exit the Switch case 13: // PRESS START - Show characters saying "Press Start" { // Local arrays like to hide inside of curly brackets or for some reason the switch doesn't work //////////////// NOTE /////////////// // Displays "Press Start" byte text[] = {B00000000, B01100111, B00000101, B01101111, B01011011, B01011011, B00000000, B00000000, B00000000, B01011011, B00001111, B01111101, B00000101, B00001111}; textOnLED(text, sizeof(text)); } eventTargetTime += 5000; // add 4000 ms to current target time to move it forward in time eventSequence ++; // reset sequence !!! break; // exit the Switch case 14: // BATTERY - Show battery State Of Charge percent, Battery Low Threshold Warning, or other related Power Information // if you wish to display the battery voltage: // Factor to calculate ADC 10-bit reading of battery voltage which has been passed through a resistive devider = (analog 10-bit test read / test measured voltage at battery) // batteryVoltage = (analogRead(BatteryVoltageRead) * Factor)); // batteryVoltage must be a 'float' variable // Application Notes: // // The Resistive Divider Factor can be found by measuring R1 (top resistor) connected to the ADC pin, and measured while not connected to the battery, // then also measuring R2 (bottom resistor) while connected between the ADC pin and ground - its value will be lower as, at 1M ohms, // there is also a lower conparative impedance through the ADC pin to ground. // The Resistive Divider Factor = 1 / ((R2 / (R1 + R2)) // // The User ADC Conversion Factor is derived by first obtaining the (10-bit) ADC reading from the analog pin via a sketch; say, "479" for example. // Then take a sample voltage reading of the output of the resistor network at that pin with your DVM; which may be at 1.78v for example. // So, The User ADC Conversion Factor = 479 / 1.78 = 269.1 [example only] // then batteryVoltage = (479 / 269.1) * 2.132 = 3.8v, which is what would be measured at the battery terminal (+) // // But wait, you say, the ADC 10-bit number should be around 364 for 1.78 volts measured. // Yes indeed, if the analog reference voltage was at 5.0v and you can certainly set a physical 5.0 reference at the AREF pin, // But on my particular Arduino Nano which is running at 5.0v (and not 7-12v) the internal Aref at the comparator sits at around 3.85v since there is a considerable voltage drop on its LDO voltage regulator. // I believe this number also changes with lower Vcc voltages, but since we are reading only a minute range (BETWEEN 3.5v - 4.2v) from a Lithium battery, // and we aren't trying to outright establish a State of Charge, we have some leeway. // // Additional Notes: When setting this up for your own system, use an adjustable bench power supply or buck/boost converter to supply a // mock typical Li-Ion/Li-Po battery voltage range of 3 to 4.1 volts. // 3.9v may typically represent the upper range of a fresh charge considering current-induced voltage drop on the cells, // and 3.6v often represents the typical battery-dead threshold for many BMSs // Some interesting discussion: // https://forum.arduino.cc/t/measuring-approximate-battery-state-of-charge-and-voltage-of-a-lipo-battery/422318/14 // https://forum.arduino.cc/t/proper-voltage-divider-for-battery-voltage-detection/398198/20 // https://jeelabs.org/2013/05/16/measuring-the-battery-without-draining-it/ // https://jeelabs.org/2013/05/17/zero-powe-battery-measurement/index.html // other display options, showing batteryVoltage or percentage ... // numJewelsLED(batteryVoltage); // Write the Battery Voltage to the second 8 x 7-segment LED module, on the right side // gameLevelLED(batteryPercent); // Write the battery charge percent to the second 8 x 7-segment LED, on the left side // TO MAKE THIS EASY : We will now just post a warning if the battery voltage falls below 3.5v ... batteryVoltage = analogRead(BatteryVoltageRead); // Read the battery voltage - OR - We can stick with just the ADC reading which is an 'int' variable (which takes less memory) // '352' as example, was roughly derived on my build as discussed above; // (3.5v * (analog 10-bit test read / test measured voltage at battery)). Your number will likely be different! if (batteryVoltage <= ADVbatteryLowThreshold && !batteryLow) { // So you say the battery is DEAD? Then let me ask you - when it was charged, was it ALIVE? batteryLow = true; // Once the threshold has been triggered, only shutting the game down and powering back up will reset this flag reducePowerForLowBattery (); // Follow through on measures to reduce power for extended low battery life but do this only once } if (batteryLow && batteryShow) { // option to show battery status in splash display lineup displayRechargeBatteryText (); // Displays the text "RECHARGE battery!" eventTargetTime += 9000; // add 5000 ms to current target time to move it forward in time (only if showing notice to Recharge Battery) } /* * Uncomment this section to see the ADC 10-bit reading on your numerical display * Use this number to derive threshold voltage number scoreBoardLED(batteryVoltage); // TEST eventTargetTime += 3000; */ eventSequence = 1; // reset sequence !!! break; // exit the Switch ///// ACTION SPECIFIC DISPLAY CASES TO FOLLOW ///// case 15: // VOLUME - Change Audio Volume { // Displays "Audio" // byte text[] = {B01110111, B00011100, B00111101, B00000100, B00011101, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000, B00000000}; // byte text[] = {B01110111, B00011100, B00111101, B00000100, B00011101}; // textOnLED(text, sizeof(text)); } gameLevelLED(Dev1Volume); // Write the current Audio level (of Music player) to 2nd Left-side LED display eventTargetTime += 2500; // add 2000 ms to current target time to move it forward in time eventSequence = 1; // reset sequence !!! break; // exit the Switch default: eventSequence ++; break; } // ...end Switch action sequence } // ...end Splash Screen loop based on timing events ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - LIGHT SHOW - Like jewels sparkling in a darkened treasure vault, or basically something to attract a player. ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (sparkleTiming < millis()) { // every (0 to 400) ms add bright star LED... 400 is arbitrary sparkleTiming = millis() + random(400); starField[random(xFieldLimit)][random(yField)] = 128; // Set random position on field for LED to be bright } for (byte x = 0; x < xField; x++) { // update LED field, darkening and randomly coloring any illuminated pixels gradually for (byte y = 0; y < yField; y++) { leds[ledIndex[x][y]] = CHSV(random(255), 255 - (starField[x][y] * random (5)), starField[x][y] / 2); starField[x][y] = starField[x][y] - random(25); if (starField[x][y] > 100) {starField[x][y] = 0;} } } FastLED.show(); ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - QUICK CONTROLS - Player Game Settings ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// menuButtonDetect(); // Detection of Menu Navigation Buttons /// NO USER INPUT for more than a minute - THEN SHUT GAME OFF! /// if (upButton || downButton || leftButton || rightButton || bothUpDownButtons || bothLeftRightButtons) { gameShutOffTime = timeTrack + 60000; // button press detected - reset player inactivity time window } else { if (timeTrack >= gameShutOffTime) { // if no button pressed for more than a minute, make sound and send signal to BMS latch // Sound Effect!!! execute_CMD(1, 0x0F, 1, 9); // Shutdown Alert Sound (Folder 01, File 009.mp3) on 1 delay(1000); // duration of sound... digitalWrite(uCShutDownPin, HIGH); // send cue to BMS to de-latch and shut down instantly!!! } } /// VOLUME ADJUST /// if (upButton || downButton) { eventTargetTime = timeTrack; eventSequence = 15; // set display to show starting level change if (upButton) { Dev1Volume++; // Increase Sound Effects volume //Dev2Volume++; // Increase Music volume } if (downButton) { Dev1Volume--; // Decrease Sound Effects volume //Dev2Volume--; // Decrease Music volume } if (!batteryLow) { // If the battery is low then audio volume adjustment will be restricted Dev1Volume = constrain(Dev1Volume, 0, 30); //Dev2Volume = constrain(Dev1Volume * 0.8, 0, 30); // Music is set here to be 80% that of the effects volume - adjust as desired. Dev2Volume = constrain(Dev1Volume * 0.95, 0, 30); // Music is set here to be ~90% that of the effects volume - adjust as desired. //Dev2Volume = constrain(Dev1Volume, 0, 30); // Music will match that of } else { Dev1Volume = constrain(Dev1Volume, 0, 15); Dev2Volume = 0; } // PRESET Volume execute_CMD(1, 0x06, 0, Dev1Volume); // Set the volume on 1 (0x00~0x30) execute_CMD(2, 0x06, 0, Dev2Volume); // Set the volume on 2 (0x00~0x30) delay(100); // Sound Effect!!! execute_CMD(1, 0x0F, 1, 3); // Game Initiate Sound (Folder 01, File 003.mp3) on 1 - Like Coin Insert // Background Music execute_CMD(2, 0x0F, 2, 7); // Yes! Yes! (Folder 02, File 007.mp3) on 2 delay(100); } /// GAME SELECT /// - Original Columns, Flash Columns, Time Trial or Crush Columns ...Press Left or Right if (leftButton || rightButton) { if (leftButton) { if (gameMode == 0) { gameMode = 3; } else { gameMode--; } } if (rightButton) { if (gameMode >= 3) { gameMode = 0; } else { gameMode++; } } eventTargetTime = timeTrack; eventSequence = 12; // set display to show the specific game title // Sound Effect!!! execute_CMD(1, 0x0F, 1, 5); // Level Up Sound! (Folder 01, File 005.mp3) on 1 delay(150); } /// RUMBLE MOTOR OPTION /// if (bothUpDownButtons) { bothUpDownButtons = false; rumbleMotor = !rumbleMotor; // Toggle option state if (rumbleMotor) { // Displays "Rumble / on" byte text[] = {B11110111, B00011100, B00010000, B00010101, B00011111, B00110000, B01101111, B00000000, B00011101, B00010101}; textOnLED(text, sizeof(text)); digitalWrite(rumbleMotorSmallPin, HIGH); delay(100); digitalWrite(rumbleMotorSmallPin, LOW); digitalWrite(rumbleMotorLargePin, HIGH); delay(100); digitalWrite(rumbleMotorLargePin, LOW); } else { // Displays "Rumble / off" byte text[] = {B11110111, B00011100, B00010000, B00010101, B00011111, B00110000, B01101111, B00000000, B00011101, B01000111, B01000111}; textOnLED(text, sizeof(text)); } delay(1000); } /// LED BRIGHTNESS/DIMMING SETTING /// if (bothLeftRightButtons && !batteryLow) { // If both [LEFT] and [RIGHT] buttons are help - and battery must not be low bothLeftRightButtons = false; LEDNumBRIGHTNESS++; // Increase the brightness for the 7-segment numerical displays... if (LEDNumBRIGHTNESS > 15) { // Cycle back to 0 if maxed out... LEDNumBRIGHTNESS = 0; } lc.setIntensity(0,LEDNumBRIGHTNESS); // Set brightness of first 7-Seg LED display lc.setIntensity(1,LEDNumBRIGHTNESS); // Set brightness of second 7-Seg LED display BRIGHTNESS = map(LEDNumBRIGHTNESS, 0, 15, 50, 255); // map(LEDNumBRIGHTNESS, 0, 15, Play Field Bright Min, Play Field Bright Max); // This sets a relative brightness scale for the Play Field's addressable LED's ///// NOTE & WARNING! ///// // A high BRIGHTNESS level could pose a potential fire hazard or cause excessive heat damage from the LEDs. // The builder assumes responsibility for finding a Brightness Max level (for the map above) that considers safety for the user, LED modules and device housing elements. // Set the Bright Min level to where the addressable LEDs are reasonably dim in appearance but still retain suitable hue/color values, especially once further reduced to 25% of that. // A combination of issues including available current/voltage supply and even the associated math will contribute to low-brightness color instability. FastLED.setBrightness(BRIGHTNESS); // Set brightness for the addressable LEDs for (int x = 0; x < xFieldLimit; x++) { for (int y = 0; y < yField; y++) { if (x < 8) { leds[ledIndex[x][y]] = CHSV(jewelHue[x + 1], 255, map(y, 0, yField, 31, 223)); // This will show entire palette of 8 jewel colors with the dimmest ones at the level of the regular game piece } } } FastLED.show(); // EEPROM memory for both variables will be see an '.update' upon a game start... // This will save on unecessary EEPROM writes as well as not letting the system get stuck with a value that // consumes too much current to allow the AVR to operate lc.clearDisplay(0); lc.clearDisplay(1); { // Displays "bright" byte text[] = {B00011111, B00000101, B00010000, B01111011, B00010111, B00001111}; textOnLED(text, sizeof(text)); } gameLevelLED(LEDNumBRIGHTNESS); // Write the current Brightness level to 2nd Left-side LED display delay(2000); // This will leave the palette display up for 2 seconds, but if 2 seconds per level shift is too slow, decrease this delay } } // ...end of While() - The game passes this point when the Start button is pressed... ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - PRE-GAME SETUP - prepares the game and game user // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (gameOver) { // This only happens after pressing start for a new game ///-///-///-/// End is at Game Loop start /// SOUND! and MUSIC! /// // Sound Effect!!! execute_CMD(1, 0x0F, 1, 3); // Game Initiate Sound (Folder 01, File 003.mp3) on 1 - Like Coin Insert //delay(1200); // A pause gets the player off the Start button... while (digitalRead(StartButtonPin) == LOW) {} // ...or just hold here until the player releases the button execute_CMD(2, 0x0E, 0, 0); // Stop music (pause) on 2 musicReady = false; // Signify that no music is playing and player 2 should be ready for random song shelfed = false; // for when we need to make a landing sound only one when moving set left or right onto resting pieces /// Variables /// xPos = xFieldLimit; // These are the starting coordinates for the bottom playSet piece // which will put the bottom of the columns stack 1 above the visible LED matrix yPos = (yField / 2) - 1; // Places the column stack just to the right of center Jewels = 0; // Temp total found during crush scan magicJewel = false; // Will be made true after a let level has been reached /// CLEAR the LED Numerical Displays and the Game Play Field /// lc.clearDisplay(0); // Clear the Numerical Displays for New Game lc.clearDisplay(1); clearPlayField (); // Clear the addressable LED Play Field with 'off' LEDs /// UPDATE SYSTEM SETTINGS /// // UPDATE player-set audio level, rumble motor option, LED brightness levels to EEPROM if applicable // if (!batteryLow) { // if system devices have been set to emergency current reduction settings then allowing these may create undesired results on future game startup, and the EEPROM may be corrupted in a brownout state EEPROM.update(8, Dev1Volume); // 1 byte - Permanently store these values EEPROM.update(9, Dev2Volume); // 1 byte EEPROM.update(17, rumbleMotor); // 1 byte - Permanently store this choice EEPROM.update(10, BRIGHTNESS); // 1 byte - Permanently set these levels EEPROM.update(11, LEDNumBRIGHTNESS); // 1 byte } /// Randomize Palette /// // A swapping random sort of the jewel palette - disable if you desire consistency // Palette entry 8 will never be swapped as it is reserved for the hardest to discriminate color and only used if playing all 8 jewels for (byte shellGame = 1; shellGame < 8; shellGame++) { // count through array - excluding the last position; 8 byte walnut = random(2, 8); // choose a random place to swap from (choose from 2 to 7) byte coconut = jewelHue[shellGame]; // pick up value at count position jewelHue[shellGame] = jewelHue[walnut]; // put the value from the random place in its place jewelHue[walnut] = coconut; // put the one from the count position in the random place } priorGameMode = gameMode; // this will cause the Splash Screen recent game score to only show the last game played according to which kind it was ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SETUP for Original Columns, Time Trial and Crush Columns Game Modes /////////////////////////////////////// if (gameMode == 0 || gameMode == 2 || gameMode == 3) { flashJewelCrushed = true; // this will keep the flash jewel from showing up in Crush Columns until after the first landing // Background Music execute_CMD(2, 0x0F, 2, 32); // Dub-Step Greetings Song (Folder 02, File 032.mp3) on 2 // First, allow player to choose the class and difficulty setting // "Class" deals with upping of the starting level along with an increase in the speed progression factor // The difficulty determines the number of jewels in play // Difficulty Pre-Set: // Class Ranking: ;) // 1 - Hermes // 2 - Ares // 3 - Hephaestus // 4 - Apollo // 5 - Hades // 6 - Poseidon // 7 - Zeus while (digitalRead(StartButtonPin) == LOW) {} // If the Start button is still being held, wait until it is un-pressed... delay(600); playerClass = 1; // This setting may be overlooked. We should retain the playing-session difficulty level (jewels in play) { // Displays "diFF. / Class" byte text[] = {B00111101, B00010000, B01000111, B11000111, B00000000, B00000000, B00000000, B00000000, B01001110, B00001110, B01110111, B01011011, B01011011, B00000000}; textOnLED(text, sizeof(text)); selectLED(0, jewelTypes - 1); // Display difficulty setting which is the number of jewels in play selectLED(1, playerClass); // Display the Player Class } timeTrack = millis() + 20000; // Give only 20 seconds (after any button press) to make decision while (digitalRead(StartButtonPin) == HIGH && timeTrack > millis()) { menuButtonDetect(); // Detection of Menu Navigation Buttons if (upButton) { // Raise the height of the pre-positioned jewel stack playerClass++; if (playerClass > 7) { playerClass = 1; } selectLED(1, playerClass); } if (downButton) { // Lower the height of the pre-positioned jewel stack playerClass--; if (playerClass < 1) { playerClass = 7; } selectLED(1, playerClass); } if (rightButton) { // Raise the difficulty level jewelTypes++; if (jewelTypes > 9) { jewelTypes = 4; } selectLED(0, jewelTypes - 1); } if (leftButton) { // Lower the difficulty level jewelTypes--; if (jewelTypes < 4) { jewelTypes = 9; } selectLED(0, jewelTypes - 1); } if (upButton || downButton || leftButton || rightButton) { timeTrack = millis() + 20000; // Sound Effect!!! execute_CMD(1, 0x0F, 1, 7); // Game Initiate Sound (Folder 01, File 007.mp3) on 1 - Bonus Warbling Sound delay(100); } } if (playerClass == 1) { gameLevelStart = 0; // Class 1 is Novice and starts at Level 0 with normal Speed-Up Factor } else { // assuming playerClass is 2 to 11 gameLevelStart = (playerClass - 1) * 5; oldDifficultyFactor = difficultyFactor; difficultyFactor = map((playerClass - 1), 2, 11, oldDifficultyFactor, 200); //constrain(difficultyFactor, 10, 250); } } // ...end of Original Columns and Time Trial Game Setup Menu ////////////////////////////////////////////////////////////////////////////////////////////////////////////// // SETUP for Flash Columns Game Mode ///////////////////////////////////////////////////////////////////////// if (gameMode == 1) { flashJewelCrushed = false; // allow flashing jewel to appear in Flash Columns // Background Music execute_CMD(2, 0x0F, 2, 60); // House Music Greetings Song (Folder 02, File 060.mp3) on 2 // First, allow player to choose the columns height and difficulty setting while (digitalRead(StartButtonPin) == LOW) {} // If the Start button is still being held, wait until it is un-pressed... delay(600); { // Displays "diFF. / High" byte text[] = {B00111101, B00010000, B01000111, B11000111, B00000000, B00000000, B00000000, B00000000, B00110111, B00010000, B01111011, B00010111}; textOnLED(text, sizeof(text)); numJewelsLED(flashColumnsHeight); // Display the default height of the Flash Columns jewel row stack selectLED(0, jewelTypes - 1); // Display difficulty setting which is the number of jewels in play } timeTrack = millis() + 20000; // Give only 20 seconds (after any button press) to make decision while (digitalRead(StartButtonPin) == HIGH && timeTrack > millis()) { menuButtonDetect(); // Detection of Menu Navigation Buttons if (upButton) { // Raise the height of the pre-positioned jewel stack // 2 is minimum, 3 below the top of field is the maximum flashColumnsHeight++; if (flashColumnsHeight > xFieldLimit - 3) { flashColumnsHeight = 2; } numJewelsLED(flashColumnsHeight); } if (downButton) { // Lower the height of the pre-positioned jewel stack flashColumnsHeight--; if (flashColumnsHeight < 2) { flashColumnsHeight = xFieldLimit - 3; } numJewelsLED(flashColumnsHeight); } if (rightButton) { // Raise the difficulty level jewelTypes++; if (jewelTypes > 9) { jewelTypes = 4; } // jewelTypes is always 1 larger to offset for the nul type at 0 selectLED(0, jewelTypes - 1); } if (leftButton) { // Lower the difficulty level jewelTypes--; if (jewelTypes < 4) { jewelTypes = 9; } selectLED(0, jewelTypes - 1); } if (upButton || downButton || leftButton || rightButton) { timeTrack = millis() + 20000; // Sound Effect!!! execute_CMD(1, 0x0F, 1, 7); // Game Initiate Sound (Folder 01, File 007.mp3) on 1 - Bonus Warbling Sound delay(100); } } } // ...end of Flash Columns Game Setup Menu // Alert The Player! //////////////////////////////////////////////////////////////////////// // Sound Effect!!! execute_CMD(1, 0x0F, 1, 15); // Coin or Points Counting Sound (Folder 01, File 015.mp3) on 1 { // Displays "Ready Set Go!" byte text[] = {B00000000, B11110111, B01101111, B01111101, B00111101, B10111011, B00000000, B00000000, B00000000, B01011011, B01101111, B10001111, B00000000, B01011110, B01111110, B10100000, B00000000}; textOnLED(text, sizeof(text)); } flashColumnsSecondsTiming = millis() + 2000; // With this, we re-purpose this variable to count the 2 seconds needed to prepare the player /// PRE-FILL Play Field Array With Random Jewels Which Have No Built-In Matches //////////////////////////////////////////////////////////////////////////////// if (gameMode == 1) { flashingJewelYPos = random(0, yField-1); // Any jewel at the bottom of the Flash Columns game can qualify. Color is not important, just whatever sits there. flashingJewelXPos = 0; byte crushField[xField][yField]; // Holds the temporary location of jewels marked for deletion as matches are weeded out bool matchFound = false; // Flag to denote need to cycle through for more matches. False when map is perfected // First fill Play Field array with random selections from the specified portion of the jewel palette list // There will be integral match-3's for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < flashColumnsHeight; x++) { // loop through x rows only to user-defined height playField[x][y] = random(1, jewelTypes); // remember that jewelTypes is 1 larger than what it points to } } ScanPreFillPlayField: { // This is a return point until there are no more matches // Scan Play Field // - - - Find matches of 3 jewels or more... for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < flashColumnsHeight; x++) { // loop through x rows only to user-defined height if (playField[x][y] > 0) { // make check only if there is a jewel at that position // Horizontal Matches ? [-] if (y > 0 && y < yField - 1 && playField[x][y] == playField[x][y-1] && playField[x][y] == playField[x][y+1]) { crushField[x][y-1] = 1; crushField[x][y] = 1; crushField[x][y+1] = 1; } // Vertical Matches ? [|] if (x > 0 && x < flashColumnsHeight - 1 && playField[x][y] == playField[x-1][y] && playField[x][y] == playField[x+1][y]) { crushField[x-1][y] = 1; crushField[x][y] = 1; crushField[x+1][y] = 1; } // "Back Slash" Diagonal ? [\] if (x > 0 && x < flashColumnsHeight - 1 && y > 0 && y < yField - 1 && playField[x][y] == playField[x-1][y-1] && playField[x][y] == playField[x+1][y+1]) { crushField[x-1][y-1] = 1; crushField[x][y] = 1; crushField[x+1][y+1] = 1; } // "Forward Slash" Diagonal ? [/] if (x > 0 && x < flashColumnsHeight - 1 && y > 0 && y < yField - 1 && playField[x][y] == playField[x+1][y-1] && playField[x][y] == playField[x-1][y+1]) { crushField[x+1][y-1] = 1; crushField[x][y] = 1; crushField[x-1][y+1] = 1; } } // ... checked for presence of jewel } // ... end loop through x rows } // ... end loop through y columns } // From Crush Field correlations, swap each found match piece for a new random jewel type for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < flashColumnsHeight; x++) { // loop through x rows only to user-defined height if (crushField[x][y] == 1) { matchFound = true; playField[x][y] = random(1, jewelTypes); // put a new choice in place of the matches found crushField[x][y] = 0; // for now we can clear the crush field (per-position) as we scan } } } // Go Back to Scan Again if any Matches were Found if (matchFound) { matchFound = false; goto ScanPreFillPlayField; // Re-Scan and try again... } // If no matches are found the map is perfected - continue // PRE-FILL The Play Field LED's with the Final Map // for (int y = 0; y < yField; y++) { for (int x = 0; x < (xFieldLimit); x++) { if (playField[x][y] > 0) { leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], 255, 64); } else { leds[ledIndex[x][y]] = CRGB(0,0,0); } } } } // ...end of Flash Columns Game Setup Menu while (millis() < flashColumnsSecondsTiming) {} // We will WAIT here unitl 2 seconds have passed from the Start button press // Since some pre-fill map resolutions can take a second or two, this will make the calculation appear seemless /// Prepare Sound and Displays /// lc.clearDisplay(0); // and clear the Top display lc.clearDisplay(1); // and clear the Bottom display // Sound Effect on the Background Music Channel execute_CMD(2, 0x0F, 1, 9); // Start Choice Button Sound (Folder 01, File 009.mp3) on 2 // So, we're playing a sound effect on the Music channel to stop the menu song and to alert the player to be ready // We set the Game Speed here as a user-set level will boost starting speed... gameSpeed = ~1000ms Initially gameSpeed = (999 * pow( 2.71828, ( (-0.001 * difficultyFactor) * (gameLevel + gameLevelStart)))) + 1 ; // Exponential decay function: milliseconds x e^( inverse rate * level) // this makes a nice speedup for most game players, bottoms out quickly but // with a taper as the game is more about skill by level 30+. // Arcade style speedup obtained by raising the rate (ie.: -0.1). lower for easy play. // Game cannot go smaller than 1 millis due to the timing and memory issues required to use micros. settleSpeed = (gameSpeed * 0.65); // ms - This factor determines how long the piece will sit before becoming permanent gameSpeedDescent = gameSpeed; // This comes into play when the Down button is pressed /// RESET SCORING VALUES for NEW GAME /// gameScore = 6000UL * gameLevelStart; // Get a bonus for starting off at higher levels - 6,000 per level gameScore = constrain(gameScore, 0, 300000); // 30,0000 pts max start off bonus //if (gameScore > 300000) {gameScore = 300000;} numJewels = 0; gameLevel = 0; /// Establish Game Scorboard Timer /// gameStartReferencePoint = millis(); // gameSeconds and game timing will use this as reference flashColon = true; if (gameMode == 2) { // for Time Trial game mode with will put 3:00 minutes on the board gameSeconds = 181; } // plus 1 sec so the display starts at full time else { gameSeconds = 0; // reset for Flash Columns game mode } if (gameMode == 0 || gameMode == 3) { // Only display the score in Original Columns and Crush Columns game modes, not for Flash Columns or Time Trial // DISPLAY (Pre-Loaded) GAME SCORE - Write to the Top LED Numerical Display // scoreBoardLED(gameScore); } else { // Only display the time in Flash Columns and Time Trial game modes... // DISPLAY GAME TIME - Write to the Left part of the Top LED Numerical Display // gameTimeLED(gameSeconds, true); } // DISPLAY (Pre-Loaded) GAME START LEVEL - Write to the Left part of the Bottom Numerical Display // gameLevelLED(gameLevel + gameLevelStart); // DISPLAY NUMBER OF JEWELS - Write to the Right part of the Bottom Numerical Display // numJewelsLED(numJewels); randomSeed(millis()); // Set the Randomizing Engine to a bit of human assisted true randomness flashColumnsSecondsTiming = millis(); // To time each "game second" during a Flash Columns or Time Trial game /// SETUP Play Pieces /// playSet[0] = random(1, jewelTypes); playSet[1] = random(1, jewelTypes); playSet[2] = random(1, jewelTypes); // Random assignments for new current play set pieces playSetNext[0] = random(1, jewelTypes); playSetNext[1] = random(1, jewelTypes); playSetNext[2] = random(1, jewelTypes); // Random assignments for preview (next) pieces showNext = true; // Allow for next preview gameOver = false; // now break and go to game... } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - GAME ACTION - The main game action - Includes timing, button detection, game piece collision detection and other actions // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Game Timing //////////////////////////////////////////////////////////////////////////////////////////////////////// // timeTrack = millis(); // META-TIMING Reference Update from the AVR - THIS IS NOW SET UP IN THE GAMECLOCK() ROUTINE gameClock(); // Call up action to keep track of game clock time and update the display - for Flash Columns and Time Trial Columns if (timeIsUp) { goto GameNowOver; // and player is done - end it now } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - PLAYER INPUT - Report Button Presses ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // "debounce" refers to both the physical amount of time allowed to authorize button input as well as input period allowance if (digitalRead(ArrangeButtonPin) == LOW && arrangeButtonAllow == true) { // Arrange button press is detected arrangeFlag = true; // tell the arrange routine to activate gameSpeedSlice = gameSpeed / 25; gameSpeedSlice = max(gameSpeedSlice, 10); // minimum amount of time timeArrangeButton = timeTrack + gameSpeedSlice; // debounce reset } if (digitalRead(ArrangeButtonPin) == HIGH && timeTrack >= timeArrangeButton) { arrangeButtonAllow = true; } if (digitalRead(LeftButtonPin) == HIGH && digitalRead(RightButtonPin) == HIGH && timeTrack >= timeLeftButton) { // Pass debounce period for the Left button leftButtonAllow = true; // general flag to allow for Left button detections - both Left and Right button must not be active } if (digitalRead(LeftButtonPin) == LOW && digitalRead(RightButtonPin) == HIGH) { // Left button press is detected while Right button is not if (leftButtonAllow) { // Left input is allowed for 1 movement of the column leftFlag = true; // tell the Left Move routine to activate //gameSpeedSlice = gameSpeed / 9; // debounce window with single presses. gameSpeed / 12.5 is suggested //gameSpeedSlice = max(gameSpeedSlice, 40); // minimum amount of time is 40ms gameSpeedSlice = map(gameSpeed, 1, 1000, 40, 111); timeLeftButton = timeTrack + gameSpeedSlice; // debounce period reset } else { // Here, we allow an exception for automatically repeated movement while holding the button if (timeTrack >= (timeLeftButton + map(gameSpeed, 1, 1000, 30, 80)) && timeTrack >= timeLeftButtonFastRepeat) { // auto-repeat pre-pause - first we wait longer, then accept shorter waits leftButtonFastAllow = true; // this also tells the Left Move routine to activate //gameSpeedSlice = gameSpeed / 25; // auto-repeat timing //gameSpeedSlice = max(gameSpeedSlice, 15); // minimum amount of time gameSpeedSlice = map(gameSpeed, 1, 1000, 10, 30); timeLeftButtonFastRepeat = timeTrack + gameSpeedSlice; // set a more rapid period } } } if (digitalRead(RightButtonPin) == HIGH && digitalRead(LeftButtonPin) == HIGH && timeTrack >= timeRightButton) { // Pass debounce period for the Right button rightButtonAllow = true; // general flag to allow for Right button detections - both Left and Right button must not be active } if (digitalRead(RightButtonPin) == LOW && digitalRead(LeftButtonPin) == HIGH) { // Right button press is detected while Left button is not if (rightButtonAllow) { // Right input is allowed for 1 movement of the column rightFlag = true; // tell the Right Move routine to activate //gameSpeedSlice = gameSpeed / 9; // debounce window with single presses. gameSpeed / 12.5 is suggested //gameSpeedSlice = max(gameSpeedSlice, 40); // minimum amount of time is 40ms gameSpeedSlice = map(gameSpeed, 1, 1000, 40, 111); timeRightButton = timeTrack + gameSpeedSlice; // debounce period reset } else { // Here, we allow an exception for automatically repeated movement while holding the button if (timeTrack >= (timeRightButton + map(gameSpeed, 1, 1000, 30, 80)) && timeTrack >= timeRightButtonFastRepeat) { // auto-repeat pre-pause - first we wait longer, then accept shorter waits rightButtonFastAllow = true; // this also tells the Right Move routine to activate //gameSpeedSlice = gameSpeed / 25; // auto-repeat timing //gameSpeedSlice = max(gameSpeedSlice, 15); // minimum amount of time gameSpeedSlice = map(gameSpeed, 1, 1000, 10, 30); timeRightButtonFastRepeat = timeTrack + gameSpeedSlice; // set a more rapid period } } } if (digitalRead(DownButtonPin) == LOW) { // Down button press was detected - Speed up descent - no debouncing needed if (!downButtonFlag) { // upon first button detect... gameSpeedDescent = gameSpeed / 35; // really fast downButtonFlag = true; // set flag so that button has to be de-pressed before being allowed back into this clause downButtonSteps = 0; // reset the step count } } else { // Down button was released - bring descent speed back to normal if (downButtonFlag) { // coming out of speed-up condition gameSpeedDescent = gameSpeed; // Reset game speed if (downButtonSteps > 0) { // Sometimes the player didn't hold down long enough for the next move to happen - No need to increase score by -1 factor gameScore = gameScore + (((downButtonSteps * 5) - 1) * (gameLevel + 1)); // add step increase to score - multiplied by level factor gameScore = constrain(gameScore, 0, 99999999); // max game score to fit on LED display // FOR DOWN BUTTON POINTS - Write to the Top LED Numerical Display - Game Score ///// if (gameMode == 0 || gameMode == 3) { scoreBoardLED(gameScore); } } // add power to eliminating Crush Bar in Crush Columns if certain criteria is met // if (gameMode == 3 && downButtonSteps > 3 && magicJewel && xPos == xFieldFloor) { stompDownFloor = xFieldFloor; // in Crush Columns, landing the Magic Jewel will cause 3 rows of the Crush Bar to be knocked down, otherwise, this number doesn't matter } downButtonSteps = 0; } downButtonFlag = false; } if (digitalRead(StartButtonPin) == LOW) { // Start button press was detected during game play - Slow down up descent - no debouncing needed if (!startButtonFlag) { // upon first button detect... gameSpeedDescent = gameSpeed * 2; // will make the game piece descesnd slower by half the speed startButtonFlag = true; // set flag so that button has to be de-pressed before being allowed back into this clause } } else { if (startButtonFlag) { // coming out of slow-down condition gameSpeedDescent = gameSpeed; // Reset game speed startButtonFlag = false; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - PLAY FIELD PIECES - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Twinkle Twinkle Magic Jewels /// if (magicJewel) { // Is our current play piece set a Magic Jewel? if (timeMagicJewel < millis()) { // Is is time to give each piece a new random color? timeMagicJewel = millis() + 10; // Set future event time if ((xPos) < xFieldLimit) { leds[ledIndex[xPos][yPos]] = CHSV(random(256), 255, 132); } // Only Play Field positions where there are physical LED's count if ((xPos+1) < xFieldLimit) { leds[ledIndex[xPos+1][yPos]] = CHSV(random(256), 255, 132); } if ((xPos+2) < xFieldLimit) { leds[ledIndex[xPos+2][yPos]] = CHSV(random(256), 255, 132); } FastLED.show(); } } /// Flashing Flash Columns Jewel /// if (gameMode == 1 || (gameMode == 3 && flashJewelCrushed == false)) { // Flashing Jewel - Integral to Flash Columns and a bonus to Crush Columns if (timeFlashingJewel < millis()) { // Is is time to give the flashing jewel a new random brightness or whiteness level? timeFlashingJewel = millis() + random(10, 60); // Set future event time if (random(0, 255) > 230) { leds[ledIndex[flashingJewelXPos][flashingJewelYPos]] = CHSV(jewelHue[playField[flashingJewelXPos][flashingJewelYPos]], random(0, 180), random(standardBright, 110)); } // occasional white flashes else { leds[ledIndex[flashingJewelXPos][flashingJewelYPos]] = CHSV(jewelHue[playField[flashingJewelXPos][flashingJewelYPos]], 255, random(16, standardBright)); } // or some darkening FastLED.show(); } } /// PREVIEW GAME PIECE /// // Reveal the Preview Game Piece Set - happens when current set has fully appeared at top of play field (xFieldLimit) if (xPos == (xFieldLimit - 3) && showNext == true) { leds[NUM_LEDS-3] = CHSV(jewelHue[playSetNext[0]], 255, standardBright); // Now show the next play piece set leds[NUM_LEDS-2] = CHSV(jewelHue[playSetNext[1]], 255, standardBright); leds[NUM_LEDS-1] = CHSV(jewelHue[playSetNext[2]], 255, standardBright); FastLED.show(); showNext = false; } /// Arrange Play Piece Set /// if (arrangeFlag) { // Sound Effect!!! // execute_CMD(1, 0x0F, 1, 10); // Arrange Shift Sound! (Folder 01, File 010.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 75, 80 ) ); // Arrange Shift Sound! // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In Folder 01, (075.mp3 is Right) to (080.mp3 is Left) on DFPlayer 1 arrangeButtonAllow = false; // Set condition for button to wait for debouncing arrangeFlag = false; if (rumbleMotor) { // small bump for arranging the play set if (!rumbleMotorSmallRequest) { // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + rumbleMotorDurationFactor; // set time event watch for small rumble motor cutoff } } byte playSetMusicalChair = playSet[0]; playSet[0] = playSet[1]; playSet[1] = playSet[2]; playSet[2] = playSetMusicalChair; if ((xPos) < xFieldLimit) { leds[ledIndex[xPos][yPos]] = CHSV(jewelHue[playSet[0]], SATURATION, standardBright); } if ((xPos+1) < xFieldLimit) { leds[ledIndex[xPos+1][yPos]] = CHSV(jewelHue[playSet[1]], SATURATION, standardBright); } if ((xPos+2) < xFieldLimit) { leds[ledIndex[xPos+2][yPos]] = CHSV(jewelHue[playSet[2]], SATURATION, standardBright); } FastLED.show(); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - PLAY - Move Columns Jewels ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Move Play Piece Set LEFT /// if (leftFlag || leftButtonFastAllow) { leftButtonAllow = false; // Set condition for button to wait for debouncing leftButtonFastAllow = false; leftFlag = false; if (yPos < (yField-1)) { // Is the play piece to the right of the left-most field column? if (playField[xPos][yPos+1] == 0) { // Are there no settled jewels in the way? if ((xPos) < xFieldLimit) { leds[ledIndex[xPos][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos][yPos+1]] = CHSV(jewelHue[playSet[0]], SATURATION, standardBright); } if ((xPos+1) < xFieldLimit) { leds[ledIndex[xPos+1][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos+1][yPos+1]] = CHSV(jewelHue[playSet[1]], SATURATION, standardBright); } if ((xPos+2) < xFieldLimit) { leds[ledIndex[xPos+2][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos+2][yPos+1]] = CHSV(jewelHue[playSet[2]], SATURATION, standardBright); } FastLED.show(); yPos++ ; // Move play set piece position 1 to the left if (playField[xPos-1][yPos] > 0) { // The set pos has moved - did we collide? if (shelfed == false) { // Have we already landed the set on a shelf? No need to make more noise if we did... // Sound Effect!!! // execute_CMD(1, 0x0F, 1, 4); // Play Piece Set Hits Ground or Other Jewel Sound (Folder 01, File 004.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 81, 86 ) ); // Landing collision sound // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In Folder 01, (081.mp3 is Right) to (086.mp3 is Left) on DFPlayer 1 if (rumbleMotor && !rumbleMotorLargeRequest) { // large bump for shelving the play set // this can only happen once unless motor was deactivated rumbleMotorLargeRequest = true; // queue a timing duration for the large rumble motor digitalWrite(rumbleMotorLargePin, HIGH); // activate large rumble motor rumbleMotorLargeTimeMilestone = timeTrack + (rumbleMotorDurationFactor / 2); // set time event watch for large rumble motor cutoff } shelfed = true; } } else { shelfed = false; // So, the set is in the air again and not on a shelf - okay to make sound next time } } else if (xPos > xFieldFloor && playField[xPos-1][yPos] == 0) { // A settled jewel is in the way and we should make a small collision sound // Sound Effect!!! execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 69, 74 ) ); // side-swipe small collision sound // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In folder 01, (069.mp3 is Right) to (074.mp3 is Left) on DFPlayer 1 if (rumbleMotor && !rumbleMotorSmallRequest) { // small bump for side-swipe collision // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + rumbleMotorDurationFactor; // set time event watch for small rumble motor cutoff } } } else if (xPos > xFieldFloor && playField[xPos-1][yPos] == 0) { // The set was asked to move at the boundary wall and we should make a small collision sound // Sound Effect!!! // execute_CMD(1, 0x0F, 1, 6); // side-swipe small collision sound (Folder 01, File 006.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, 74); // side-swipe small collision sound [Full Left] (Folder 01, File 074.mp3) on 1 if (rumbleMotor && !rumbleMotorSmallRequest) { // small bump for side-swipe collision // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + rumbleMotorDurationFactor; // set time event watch for small rumble motor cutoff } } } /// Move Play Piece Set RIGHT /// if (rightFlag || rightButtonFastAllow) { rightButtonAllow = false; // Set condition for button to wait for debouncing rightButtonFastAllow = false; rightFlag = false; if (yPos > 0) { // Is the play piece to the left of the right-most field column? if (playField[xPos][yPos-1] == 0) { // Are there no settled jewels in the way? if ((xPos) < xFieldLimit) { leds[ledIndex[xPos][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos][yPos-1]] = CHSV(jewelHue[playSet[0]], SATURATION, standardBright); } if ((xPos+1) < xFieldLimit) { leds[ledIndex[xPos+1][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos+1][yPos-1]] = CHSV(jewelHue[playSet[1]], SATURATION, standardBright); } if ((xPos+2) < xFieldLimit) { leds[ledIndex[xPos+2][yPos]] = CRGB(0,0,0); leds[ledIndex[xPos+2][yPos-1]] = CHSV(jewelHue[playSet[2]], SATURATION, standardBright); } FastLED.show(); yPos-- ; // Move play set piece position 1 to the right // Sound Effect!!! if (playField[xPos-1][yPos] > 0) { // The set pos has moved - did we collide? if (!shelfed) { // Have we already landed the set on a shelf? No need to make more noise if we did... // execute_CMD(1, 0x0F, 1, 4); // Play Piece Set Hits Ground or Other Jewel Sound (Folder 01, File 004.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 81, 86 ) ); // Landing collision sound // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In Folder 01, (081.mp3 is Right) to (086.mp3 is Left) on DFPlayer 1 if (rumbleMotor && !rumbleMotorLargeRequest) { // large bump for shelving the play set // this can only happen once unless motor was deactivated rumbleMotorLargeRequest = true; // queue a timing duration for the large rumble motor digitalWrite(rumbleMotorLargePin, HIGH); // activate large rumble motor rumbleMotorLargeTimeMilestone = timeTrack + (rumbleMotorDurationFactor / 2); // set time event watch for large rumble motor cutoff } shelfed = true; } } else { shelfed = false; // So, the set is in the air again and not on a shelf - okay to make sound next time } } else if (xPos > xFieldFloor && playField[xPos-1][yPos] == 0) { // A settled jewel is in the way and we should make a small collision sound // Sound Effect!!! execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 69, 74 ) ); // side-swipe small collision sound // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In Folder 01, (069.mp3 is Right) to (074.mp3 is Left) on DFPlayer 1 if (rumbleMotor && !rumbleMotorSmallRequest) { // small bump for side-swipe collision // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + rumbleMotorDurationFactor; // set time event watch for small rumble motor cutoff } } } else if (xPos > xFieldFloor && playField[xPos-1][yPos] == 0) { // The set was asked to move at the boundary wall and we should make a small collision sound // Sound Effect!!! // execute_CMD(1, 0x0F, 1, 6); // side-swipe small collision sound (Folder 01, File 006.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, 69); // side-swipe small collision sound [Full Right] (Folder 01, File 069.mp3) on 1 if (rumbleMotor && !rumbleMotorSmallRequest) { // small bump for side-swipe collision // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + rumbleMotorDurationFactor; // set time event watch for small rumble motor cutoff } } } /// Progress Game Piece /// if (timeTrack >= ( timePlaySet + (gameSpeedDescent * (0.68 + (xPos * 0.025))) ) ) { // First see if enough time has passed to progress game piece // if (timeTrack >= ( timePlaySet + gameSpeedDescent ) { // Us this line instead for a no Gravitational Acceleration option // The gameSpeed(Descent) was reduced as xPos decreases to give a slight feel of gravitational acceleration timePlaySet = timeTrack; // Reset timer on play piece march if (xPos > xFieldFloor) { // Is the play piece above ground? if (playField[xPos-1][yPos] == 0) { // This sees if there is a free space under the play piece settleFlag = false; // If the piece was moved and nothing is underneath then no settling xPos-- ; // march descent on play piece // Sound Effect!!! if (xPos == xFieldFloor || playField[xPos-1][yPos] > 0) { // The set pos has moved - did we collide with another jewel or hit the floor? // execute_CMD(1, 0x0F, 1, 4); // Play Piece Set Hits Ground or Other Jewel Sound (Folder 01, File 004.mp3) on 1 [Non-Stereo] execute_CMD(1, 0x0F, 1, map(yPos, 0, (yField - 1), 81, 86 ) ); // Landing collision sound // we use a pre-made stereo image of this sound, mapped to yPos, scaled to the desired playfield size - In Folder 01, (081.mp3 is Right) to (086.mp3 is Left) on DFPlayer 1 if (rumbleMotor && !rumbleMotorLargeRequest) { // large bump for landing the play set // this can only happen once unless motor was deactivated rumbleMotorLargeRequest = true; // queue a timing duration for the large rumble motor digitalWrite(rumbleMotorLargePin, HIGH); // activate large rumble motor rumbleMotorLargeTimeMilestone = timeTrack + (rumbleMotorDurationFactor * 3); // set time event watch for large rumble motor cutoff } } if (downButtonFlag == true) { // if Down Button pressed then increase count downButtonSteps++; } if ((xPos) < xFieldLimit) { leds[ledIndex[xPos][yPos]] = CHSV(jewelHue[playSet[0]], SATURATION, standardBright); } if ((xPos+1) < xFieldLimit) { leds[ledIndex[xPos+1][yPos]] = CHSV(jewelHue[playSet[1]], SATURATION, standardBright); } if ((xPos+2) < xFieldLimit) { leds[ledIndex[xPos+2][yPos]] = CHSV(jewelHue[playSet[2]], SATURATION, standardBright); } if ((xPos+3) < xFieldLimit) { leds[ledIndex[xPos+3][yPos]] = CRGB(0,0,0); } FastLED.show(); } else { // Was there something under the play piece? - Raise settleFlag if (!settleFlag) { timeSettle = timeTrack; } settleFlag = true; } } else { // Piece is now sitting on the floor, Raise settleFlag if (!settleFlag) { timeSettle = timeTrack; } settleFlag = true; } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - MISC - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Give Life to the Crush Bar /// if (gameMode == 3 && xFieldFloor > 0) { // Glitter up the dance floor! if (timeCrushBarGlitter < millis()) { // Is is time to give the Crush Bar a new coat of paint? timeCrushBarGlitter = millis() + 15; // Set future event time floorRainbowIndex++; // cycle through LED hue color rainbow - will reset to 0 once overflowed (> 255) byte floorRainbowIndexShift; // this gives the wall some variation that rolls with the effect for (byte y = 0; y < yField; y++) { // loop through y columns in the Play Field for (byte x = 0; x < xFieldFloor; x++) { // loop up through x rows within each column in the Play Field up to the Crush Bar floor line if (x < xFieldLimit) { floorRainbowIndexShift = floorRainbowIndex + (random(6) * x); leds[ledIndex[x][y]] = CHSV(floorRainbowIndex, 120, standardBright / 2); // pre-load Crush Bar Play Field LEDs with dimmer rainbow pastel blocks leds[ledIndex[random(0, xFieldFloor)][random(0, yField)]] = CHSV(floorRainbowIndexShift, 200, standardBright / 2); // add some spice } } } FastLED.show(); } } /// Rumble Motor Timed Cutoff /// // if the Rumble Motor option is allowed and the small rumble motor was activated and the timing request for activation of the small rumble motor has expired... if (rumbleMotor && rumbleMotorSmallRequest && timeTrack >= rumbleMotorSmallTimeMilestone) { digitalWrite(rumbleMotorSmallPin, LOW); // deactivate small rumble motor rumbleMotorSmallRequest = false; // request fulfilled } // if the Rumble Motor option is allowed and the large rumble motor was activated and the timing request for activation of the large rumble motor has expired... if (rumbleMotor && rumbleMotorLargeRequest && timeTrack >= rumbleMotorLargeTimeMilestone) { digitalWrite(rumbleMotorLargePin, LOW); // deactivate large rumble motor rumbleMotorLargeRequest = false; // request fulfilled } /// BACKGROUND MUSIC - - - PLAY The Next Background Music Track /// if (digitalRead(MusicBusyPin) == HIGH && !musicReady) { // Busy Pin - LOW means playing, HIGH means not playing musicReady = true; if ((gameMode == 0 && gameLevel <= 15) || gameMode == 1) { // The "Columns" music collection will be chosen from if within the first 15 levels of Original Columns of during all levels of Flash Columns do { musicChoice = random(28, 131); // Choose a song .mp3 file index number between 028 and 130 } while (musicPriorChoice == musicChoice || (musicChoice > 108 && musicChoice < 114)); // Avoid last song and re-choose if in the Event Music index range (See bottom of sketch) musicPriorChoice = musicChoice; // We'll only avoid duplicates of Columns songs to save on sketch memory execute_CMD(2, 0x0F, 2, musicChoice); // Song - (Play a random song in Folder 02, from 028.mp3 to 108.mp3 or 114.mp3 to 130.mp3) on 2 } else { if ((gameMode == 0 && gameLevel > 15) || gameMode == 3) { // The Tracker/ChipTune/KeyGen/Installer music collection will be chose from for levels 16 and above of Original Columns and during all levels of Crush Columns execute_CMD(2, 0x0F, 3, random(01, 256)); // Choose a song .mp3 file index number between 001 and 255 in Folder 03 } } if (gameMode == 2) { // Time Trial Columns gets its own small collection of 3-minute timed pacing songs execute_CMD(2, 0x0F, 4, random(01, 16)); // Choose a song .mp3 file index number between 001 and 015 in Folder 04 // Operation Note: Because these songs are exactle 3 minutes, the length of the Time Trial game, this should not be repeated } } if (digitalRead(MusicBusyPin) == LOW) { musicReady = false; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - GAME CONSEQUENCE - If piece set stops progressing - Scan, Collapse, Score, Repeat, or Lose // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///-///-///-///-/// The following 3 opening brackets encompass everything down to the last Void-included section ///-///-///-///-/// /// CHECK TO SEE IF THE PLAY PIECE HAS LANDED AND SETTLED /// if (settleFlag) { // Here, you write the piece to playField, reset start xPos and yPos, // set up next piece set and perform scans and checks // This is when time runs out for current play piece - wait longer if in Down mode settleSpeed = (gameSpeed * 0.65); // ms - This factor determines how long (extra over standard progress increment) the piece will sit before becoming permanent if (timeTrack >= (timeSettle+settleSpeed) || (gameSpeedDescent != gameSpeed) && timeTrack >= (timeSettle+(settleSpeed * 2))) { settleFlag = false; bool xPosBounds = false; if (xPos > 0) { if (playField[xPos-1][yPos] > 0) { xPosBounds = true; }} // the Play Field array can't be checked out of its bounds, so test this first if (xPos == xFieldFloor || xPos == 0 || xPosBounds == true) { // Make sure that after a Move that you are not in the air again, if you are on the ground or on top of a jewel then proceed to end... byte crushField[xField][yField]; // Holds the temporary location of jewels marked for deletion // and score tallying after scanning for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xField; x++) { // loop through x rows crushField[x][y] = 0; // reset crush field array, or at least the memory where it resides } } if (xPos > xFieldFloor) { magicTouch = playField[xPos-1][yPos]; // if Magic Jewels touch the ground then nothing happens, if they land on another Jewel - record that Jewel } else { magicTouch = 0; } playField[xPos][yPos] = playSet[0]; playField[xPos+1][yPos] = playSet[1]; playField[xPos+2][yPos] = playSet[2]; xPos = xFieldLimit; // These are the starting coordinates for the bottom jewel of the playSet columns piece // which will put the bottom of the columns stack 1 row above the visible LED matrix yPos = (yField / 2) - 1; // This places the column stack just to the right of center playSet[0] = playSetNext[0]; playSet[1] = playSetNext[1]; playSet[2] = playSetNext[2]; randomSeed(millis()); playSetNext[0] = random(1, jewelTypes) ; playSetNext[1] = random(1, jewelTypes) ; playSetNext[2] = random(1, jewelTypes) ; if (downButtonFlag) { // coming out of speed-up condition - - - Settle up score and reset Down Button sequence gameSpeedDescent = gameSpeed; if (downButtonSteps > 0) { // Sometimes the player didn't hold down long enough for the next move to happen gameScore = gameScore + (((downButtonSteps * 2) - 1) * (gameLevel + 1)); // add step increase to score - multiplied by level factor gameScore = constrain(gameScore, 0, 99999999); // max game score to fit on LED display // FOR DOWN BUTTON POINTS... // write to the first LED Numerical Display - Game Score if (gameMode == 0) { scoreBoardLED(gameScore); } // only for Original Columns } downButtonSteps = 0; downButtonFlag = false; } crushCycle = 1; // Prepare Jewel Score multiplier for a chain reaction - making for larger Combo Bonus payouts! ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - SCAN... - Scan Play Field in all directions and build up Crush Field - Jewel by Jewel ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ScanPlayField: { /// Scan Play Field /// - - - Find matches of 3 jewels or more... if (!magicJewel) { // Scan procedure when no Magic Jewel is present - - - This only takes 2 ms per scan for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xField; x++) { // loop through x rows if (playField[x][y] > 0) { // make check only if there is a jewel at that position // Horizontal Matches ? [-] if (y > 0 && y < yField - 1 && playField[x][y] == playField[x][y-1] && playField[x][y] == playField[x][y+1]) { crushField[x][y-1] = 1; crushField[x][y] = 1; crushField[x][y+1] = 1; } // Vertical Matches ? [|] if (x > 0 && x < xField - 1 && playField[x][y] == playField[x-1][y] && playField[x][y] == playField[x+1][y]) { crushField[x-1][y] = 1; crushField[x][y] = 1; crushField[x+1][y] = 1; } // "Back Slash" Diagonal ? [\] if (x > 0 && x < xField - 1 && y > 0 && y < yField - 1 && playField[x][y] == playField[x-1][y-1] && playField[x][y] == playField[x+1][y+1]) { crushField[x-1][y-1] = 1; crushField[x][y] = 1; crushField[x+1][y+1] = 1; } // "Forward Slash" Diagonal ? [/] if (x > 0 && x < xField - 1 && y > 0 && y < yField - 1 && playField[x][y] == playField[x+1][y-1] && playField[x][y] == playField[x-1][y+1]) { crushField[x+1][y-1] = 1; crushField[x][y] = 1; crushField[x-1][y+1] = 1; } } // ... checked for presence of jewel } // ... end loop through x rows } // ... end loop through y columns } // ...end scanning with no magic jewel else { // Scan procedure if a Magic Jewel touches down... // Scan Play Field and mark all Jewels matching the one the Magic Jewel landed on // These will be crushed and deleted for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xField; x++) { // loop through x rows if (playField[x][y] > 0 && (playField[x][y] == magicTouch || playField[x][y] == 9)) { crushField[x][y] = 1; } } } stompDownFloor = 2; // in Crush Columns, landing the Magic Jewel will cause 3 rows of the Crush Bar to be knocked down, otherwise, this number doesn't matter magicJewel = false; // We're done with the Magic Jewel - allow for collapse, then scan and collapse again... gameScore += 10000; // 10,000 pts for magic jewel set landing gameScore = constrain(gameScore, 0, 99999999); // max game score to fit on LED display } /// Check to see if the Flashing Jewel in the Flash Columns game mode was a casualty //////////////// if (gameMode == 1 && crushField[flashingJewelXPos][flashingJewelYPos] == 1) { flashJewelCrushed = true; } if (gameMode == 3 && crushField[flashingJewelXPos][flashingJewelYPos] == 1 && flashJewelCrushed == false) { // flashJewelCrushed can only be made 'false' within the Collapse section when an available place is found flashJewelCrushed = true; // flags to set a new position for a new flash jewel stompDownFloor = xFieldFloor; // crushing this jewel in Crush Columns will eliminate the Crush Bar gameScore += (xFieldFloor * 300 * (10 * (gameLevel + 1))); // award a lot of points! //gameScore += (xFieldFloor * 1000 * (30 * (gameLevel + gameLevelStart))); // award a boat load of points! } /// Temporary Variables /// bool crushSound = false; // reset flag to say there was a crushed jewel found int crushColumnCount[yField]; // array to remember per-column (in play field) total count of crushed jewels float crushTotal = 0; // total number found in crush field array float crushPositionalAmplitude = 0; // factor by which weighted mean/median will be implemented float crushStereoLocation = 0; // this is where the (pre-formed) stereo sound should be directed to emerge int jewelCrushSoundCycleLevel = 0; // crush cycle sound bank multiplyer int jewelCrushSoundFileBase = 87; // this is 0087.mp3 as base track - - - see Sound List at bottom of sketch int jewelCrushSoundFilePointer = jewelCrushSoundFileBase; // and on... Jewels = 0; // Reset count before totalling number of new jewels smashed in this scan event /// CRUSH FIELD SCAN and TALLY ////////////////////////////////// for (byte y = 0; y < yField; y++) { // loop through y columns crushColumnCount[y] = 0; // reset count per column scanned for (byte x = 0; x < xField; x++) { // loop through x rows if (crushField[x][y] == 1) { crushSound = true; // if any crushed jewel is found in entire scan then flag that we have at least one instance crushColumnCount[y]++; // increase tally per column scanned Jewels++; // add Jewels to temp total - for scoring } } crushTotal += crushColumnCount[y]; // sum up all instances found in crush field crushPositionalAmplitude += crushColumnCount[y] * (y + 1); // add weighted sums of each column, also the +1 keeps the first position from being 0 } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - CRUSH SOUND - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Check to see if we should make a crushed jewel sound effect - so that it will play while jewels twinkle out //////// /// Play Sound /// if (crushSound) { // if any crushed jewel was found... crushStereoLocation = round(crushPositionalAmplitude / crushTotal) - 1; // find the weighted median/mean, also -1 to match array (0-5) if (crushStereoLocation < 0) { crushStereoLocation = 0; } // just in case some bad math happens if (crushStereoLocation >= yField) { crushStereoLocation = yField - 1; } // Sound Effect!!! (0020.mp3 to 0023.mp3) on 1 [for Non-Stereo], (0087.mp3 - 0110.mp3) on 1 [for Stereo] if (crushCycle <= 1) { jewelCrushSoundCycleLevel = 0; // execute_CMD(1, 0x0F, 1, 20); // Crush Jewel Sound - Normal [Uncomment for Non-Stereo] } if (crushCycle == 2) { jewelCrushSoundCycleLevel = 1; // execute_CMD(1, 0x0F, 1, 21); // Crush Jewel Sound - Super [Uncomment for Non-Stereo] } if (crushCycle == 3) { jewelCrushSoundCycleLevel = 2; // execute_CMD(1, 0x0F, 1, 22); // Crush Jewel Sound - Hyper [Uncomment for Non-Stereo] } if (crushCycle >= 4) { jewelCrushSoundCycleLevel = 3; // execute_CMD(1, 0x0F, 1, 23); // Crush Jewel Sound - Insane [Uncomment for Non-Stereo] stompDownFloor = 4; // in Crush Columns, achieving the feat of a 4+chain reaction will cause 4 rows of the Crush Bar to be knocked down per 4+, otherwise, this number doesn't matter } jewelCrushSoundFilePointer += (jewelCrushSoundCycleLevel * 6) + crushStereoLocation ; // i.e.: 0087 + (cycle multiplier is 0 * 6 files per stereo sound set) + 0 to 5 stereo positions if (jewelCrushSoundFilePointer < jewelCrushSoundFileBase || jewelCrushSoundFilePointer >= (jewelCrushSoundFileBase + 24)) { jewelCrushSoundFilePointer = jewelCrushSoundFileBase; // who knows... } execute_CMD(1, 0x0F, 1, jewelCrushSoundFilePointer); // Crush Jewel Sound - [Pre-Panned Articulated samples for Stereo] In Folder 01 if (rumbleMotor) { // Super bump for crushing jewels! if (!rumbleMotorLargeRequest) { // this can only happen once unless motor was deactivated rumbleMotorLargeRequest = true; // queue a timing duration for the large rumble motor digitalWrite(rumbleMotorLargePin, HIGH); // activate large rumble motor rumbleMotorLargeTimeMilestone = timeTrack + (rumbleMotorDurationFactor * 2); // set time event watch for large rumble motor cutoff } if (!rumbleMotorSmallRequest) { // this can only happen once unless motor was deactivated rumbleMotorSmallRequest = true; // queue a timing duration for the small rumble motor digitalWrite(rumbleMotorSmallPin, HIGH); // activate small rumble motor rumbleMotorSmallTimeMilestone = timeTrack + (rumbleMotorDurationFactor * 2); // set time event watch for large rumble motor cutoff } } } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - TWINKLE - Let's make the crushed pieces LEDs twinkle out! ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Randomly Fade Out LEDs /// for (byte i = 64; i > 0; i--) { // cycle for a number of instances... for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xFieldLimit; x++) { // loop through x rows as available for the LED portion if (crushField[x][y] == 1) { // if this location is where a match was found... leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], SATURATION, i); // Fade out LED with random white levels if (random(11) > 8) { // pacing of occasional flashes are relatively seldom... // Add random super-bright flashes of 4x white leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], random(SATURATION+1), random(256-i)); } } } } FastLED.show(); delay(5); // adjust for pacing } gameClock(); // Call up action to keep track of game clock time and update the display - for Flash Columns and Time Trial Columns /// Finally, CLEAR the LEDs and take note of the types of Jewels crushed and also reset the arrays /// for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xField; x++) { // loop through x rows if (crushField[x][y] == 1) { // this is where a match was found... if (x < xFieldLimit) { // need to write to existing LEDs within the Play Field only leds[ledIndex[x][y]] = CRGB(0,0,0); // delete Jewels from the Play Field LED's array } jewelTypesFound[playField[x][y]] = 1; // mark the jewel types that were found to have matches crushField[x][y] = 0; // reset crush field array (piecemeal method) playField[x][y] = 0; // DELETE Jewels that are crushed } } } numJewels += Jewels; // numJewels will get written to numerical display and Jewels can be used as well FastLED.show(); // Rumble Motors Cutoff - - - let's not keep the motors running... if (rumbleMotor) { // if the Rumble Motor option is allowed... digitalWrite(rumbleMotorSmallPin, LOW); // deactivate small rumble motor rumbleMotorSmallRequest = false; // request fulfilled digitalWrite(rumbleMotorLargePin, LOW); // deactivate large rumble motor rumbleMotorLargeRequest = false; // request fulfilled } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - SCORE! - Update Scoring for Added Jewel Crushes, Also Levels and other Bonuses ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (Jewels > 0) { // Let's get some points on the board for crushing jewels! /// Nominal Score Increase - - - Determined by number of jewels crushed /// // gameScore += (Jewels * ((gameLevel + gameLevelStart + 1) * 10 * crushCycle)); // Each jewel: 10 pts x level x number of chain reactions gameScore += (Jewels * ((gameLevel + gameLevelStart + 1) * (10 * (gameMode + 1)) * crushCycle)); // Each jewel: 10 pts x level x number of chain reactions - the Game Mode factors in /// Add COMBO BONUS for matching more than one kind /// for (byte i = 1; i < 10; i++) { // go through array... if (jewelTypesFound[i] > 0) { // was a type of jewel marked? jewelTypesFoundTotal++ ; // then add to the total jewelTypesFound[i] = 0; // clear the marking array } } if (jewelTypesFoundTotal > 1) { // award bonus for matching more than one type of jewel if (jewelTypesFoundTotal == 2) { gameScore += (50 * (jewelTypes - 1)) ; // 300 pts for matching 2 kinds at once - more luck than skill } if (jewelTypesFoundTotal == 3) { gameScore += (170 * (jewelTypes - 1)) ; // 1000 pts for matching 3 kinds at once - more skill than luck } if (jewelTypesFoundTotal > 3) { gameScore += ( (170 * (jewelTypes - 1)) + ((constrain((jewelTypesFoundTotal - 3), 0, 9)) * (100 * (jewelTypes - 1)))) ; // this is generally due to a chain reaction // Sound Effect!!! execute_CMD(1, 0x0F, 1, 7); // Bonus Sound! (Folder 01, File 007.mp3) on 1 - This may be cut short by following sound requests... } } jewelTypesFoundTotal = 0 ; // reset the count gameScore = constrain(gameScore, 0, 99999999); // max game score to fit on LED display /// Update Level - - - If score reaches certain benchmarks /// int oldGameLevel = gameLevel; if (numJewels > 10) { if (gameMode == 2) {gameLevel = ((numJewels-10)/9 ); } // game level is plainly based on the number of crushed jewels (every 9-ish) - Time Trial gets up in speed faster if player scores more else { gameLevel = ((numJewels-10)/25); } // game level is plainly based on the number of crushed jewels (every 25-ish) } numJewels = constrain(numJewels, 0, 9999); // max jewels crushed total to fit on LED display constrain(gameLevel, 0, 999 - gameLevelStart); // Limit the Game Level to the display max /// Update Game Speed - - - Level determines speed, not score /// if (gameLevel > oldGameLevel) { gameSpeed = (999 * pow( 2.71828, ( (-0.001 * difficultyFactor) * (gameLevel + gameLevelStart)))) + 1 ; // exponential decay function: milliseconds x e^( inverse rate * level) // This makes a nice speedup for most game players, bottoms out quickly but // with a taper as the game advances in level, which is then more about skill. // Arcade style speedup obtained by raising the rate (ie.: -0.1). lower for easier play. // Game cannot go smaller than 1 millis. // Sound Effect!!! execute_CMD(1, 0x0F, 1, 5); // Level Up Sound! (Folder 01, File 005.mp3) on 1 stompDownFloor = 2; // in Crush Columns, upgrading to the next level will cause 2 rows of the Crush Bar to be knocked down, otherwise, this number doesn't matter if (gameMode == 0 || gameMode == 3) { magicLevelAllotment = true; // if level has increased then allow for another Magic Jewel } else { magicLevelAllotment = false; // do not allow for a Magic Jewel occurance if the game mode is not Original Columns or Crush Columns } } /// DISPLAY GAME SCORE - Write to the first LED Numerical Display /// if (gameMode == 0 || gameMode == 3) { scoreBoardLED(gameScore); } /// DISPLAY LEVEL - Write to the Left part of the second Numerical Display /// gameLevelLED(gameLevel + gameLevelStart); /// DISPLAY NUMBER OF JEWELS - Write to the Right part of the second Numerical Display /// numJewelsLED(numJewels); } // finish updating score } // ...end ScanPlayField ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - COLLAPSE - Progressively compress play field, eliminating new holes ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Collapse: { /// Mind The Gap /// collapsePositive = false; // This flag is true if a collapse has happend at least once in the entire field scan // if false, then nothing was collapsed, finished with all crush scans bool baseJewels; // This flag is true when scan of first gap from the bottom runs into another Jewel above byte newFlashingJewelYPos = random(0, yField); // when placing a new flash jewel for Crush Columns, we'll first pick a random y pos, if no jewels are there then we'll wait until another landing byte findHeight = 0; // upon finishing collapsing a single column, this will give the mean height range to place a new random flash jewel for Crush Columns for (byte y = 0; y < yField; y++) { // loop through y columns baseJewels = false; // this flag is true when a scan for the first gap from the bottom runs into another jewel above for (byte x = xFieldFloor; x < (xField-1); x++) { // go up each column (x row), not including the top row or Crush Bar area if (playField[x][y] == 0) { // was there a hole (or the top of any column) baseJewels = true; // flag that the first hole was found } if (baseJewels == true) { if (flashJewelCrushed == false && gameMode == 3 && x+1 == flashingJewelXPos && y == flashingJewelYPos) { // is the new airborne jewel the flashing jewel? This was not yet a crush casualty flashingJewelXPos = x; } playField[x][y] = playField[x+1][y]; // copy every higher up Jewel to its lower neighbor if (playField[x][y] > 0) { collapsePositive = true; // something was found in the Field above a gap } } } playField[xField-1][y] = 0; // the top virtual space cleared // Find a new home for a new flashing jewel - if none found, try again next time // if (gameMode == 3 && flashJewelCrushed == true && y == newFlashingJewelYPos) { // we will attempt to choose a location for the next flash jewel in Crush Columns, the previous one was a crush casualty bool foundSpot = false; for (byte x = xFieldFloor; x < (xField-1); x++) { // loop through a newly collapsed stack to count the height so we can get a range if (playField[x][y] > 0) { findHeight = x; foundSpot = true; } } if (foundSpot) { flashingJewelXPos = random (xFieldFloor, findHeight + 1); flashingJewelYPos = newFlashingJewelYPos; flashJewelCrushed = false; // 'false' means we now have an existing flashing jewel } } } /// REDRAW The Play Field LEDs /// for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = xFieldFloor; x < (xFieldLimit); x++) { // loop through x rows in the LED field only corresponding to the area above the Crush Bar if (playField[x][y] > 0) { leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], 255, standardBright); } else { leds[ledIndex[x][y]] = CRGB(0,0,0); } } } FastLED.show(); gameClock(); // Call up action to keep track of game clock time and update the display - for Flash Columns and Time Trial Columns delay(50); // Governs how fast the stack appears to fall while collapsing } // end Collapse procedure area if (collapsePositive) { // keep jewel stack collapsing until there are no jewels floating in the air... goto Collapse; } if (Jewels > 0) { // jewels found earlier indicates a possibility of a future chain reaction - go check again... crushCycle++; // keep track of how many times we did this goto ScanPlayField; // if crushes were made once, go back up and check again... } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - CRUSH BAR - Insert bar level on random intervals, remove upon player actions ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (gameMode == 3) { // Only allow the Crush Bar in the Crush Columns game /// RAISE CRUSH BAR - - - The small chance for a Crush Bar row to be added after every jewel crushing & collapse /// int crushBarChance = map( gameSpeed, 0, 1001, 520, 820 ); // Game Speed determines severity of chance if ( random(1001) >= crushBarChance ) { // Upon a random chance which increases the more we play - // Insert a Crush Bar! /// Scroll Play Field UP 1 Row /// if (xFieldFloor < xField - 1) { xFieldFloor++; // Raise the Play Field floor boundary up 1 flashingJewelXPos++; // also raise the flashing jewel - if it so be that the flashing jewel is pushed above the top, the game will end before anyways } for (byte y = 0; y < yField; y++) { // loop through y columns in the Play Field for (int x = xField - 1; x > -1; x--) { // loop backwards through x rows within each column in the Play Field (i.e. 15 to 0) if (x > 0) { playField[x][y] = playField[x-1][y]; // copy jewel attribute from one row below - any jewels now sitting in the Soft Zone will be caught by Flood detection } else { playField[0][y] = 0; // clear bottom row } if (x >= xFieldFloor && x < xFieldLimit) { if (playField[x][y] > 0) { leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], 255, standardBright); // pre-load active jewel area Play Field LEDs } else { leds[ledIndex[x][y]] = CRGB(0,0,0); } } else if (x < xFieldLimit) { leds[ledIndex[x][y]] = CHSV(random(256), 200, standardBright / 2); // pre-load Crush Bar Play Field LEDs with dimmer pastel blocks } } } FastLED.show(); // Sound Effect! // execute_CMD(1, 0x0F, 1, 4); // Large landing sound - non-stereo, Folder 01, File 004.mp3 delay(200); // slight pause /// Did The Crush Bar Top Out? /// if ( xFieldFloor >= xFieldLimit ) { // If the floor reaches the top, GAME OVER! crushedByFloor = true; goto GameNowOver; } } // ...end raising Crush Bar 1 row /// LOWER CRUSH BAR - - - We can reduce the number of Crush Bar rows by specification upon certain actions like collecting a number of jewels, catching a flashing jewel or using the magic jewel stompDownFloor = min(stompDownFloor, xFieldFloor); // this ensures that the floor doesn't go under the bottom, taking some jewels with it. // omit this line if being able to push a few rows of jewels out of existence is a game-play benefit for (byte i = 0; i < stompDownFloor; i++) { // iterate number of specified times /// Scroll Play Field DOWN 1 Row * Loop /// if (xFieldFloor > 0) { // down-scrolling can only happen if the Crush Bar is above the bottom Play Field position xFieldFloor--; // Lower the Play Field floor boundary down 1 flashingJewelXPos--; // also lower the flashing jewel along with the rest for (byte y = 0; y < yField; y++) { // loop through y columns in the Play Field for (byte x = 0; x < xField; x++) { // loop up through x rows within each column in the Play Field if (x < xField - 1) { // can't take from above top row playField[x][y] = playField[x+1][y]; // copy jewel attribute from one row above } else { playField[xField - 1][y] = 0; // add null to top row of Play Field array } if (x >= xFieldFloor && x < xFieldLimit) { if (playField[x][y] > 0) { leds[ledIndex[x][y]] = CHSV(jewelHue[playField[x][y]], 255, standardBright); // pre-load active jewel area Play Field LEDs } else { leds[ledIndex[x][y]] = CRGB(0,0,0); } } else if (x < xFieldLimit) { leds[ledIndex[x][y]] = CHSV(random(256), 200, standardBright / 2); // pre-load Crush Bar Play Field LEDs with dimmer pastel blocks } } } FastLED.show(); // Sound Effect! // execute_CMD(1, 0x0F, 1, 4); // Large landing sound - non-stereo, Folder 01, File 004.mp3 delay(200); // slight pause for DFPlayer processing and to give the wall stomping a perceptible procession } } stompDownFloor = 0; // reset this flag } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - Emergency Reduced Power - if the battery reaches the lower voltage threshold during game play, reduce current draw! //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// batteryVoltage = analogRead(BatteryVoltageRead); // Read the battery voltage if (batteryVoltage <= ADVbatteryLowThreshold && !batteryLow) { // So you say the battery is DEAD? Then let me ask you - when it was charged, was it ALIVE? batteryLow = true; // Once the threshold has been triggered, only shutting the game down and powering back up will reset this flag reducePowerForLowBattery (); // Follow through on measures to reduce power for extended low battery life but do this only once displayRechargeBatteryText (); // Displays a one-time warning to "RECHARGE battery!" delay(2000); // A sudden shift in lighting and sound, especially when the player is 'in the zone' may be a bit unerving, so pausing a bit is important for this message lc.clearDisplay(0); // Clear the displays to allow for the future posting of score, time and etc. lc.clearDisplay(1); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // - FLOODED? - if the stack of jewels reach the top - then Game Over! //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// bool flooded = false; // this flag waves if the game is now over (pieces have settled prior) FloodCheck: { // See if any Jewels remain in the non-visible virtual zone at the top - just scan xFieldLimit line for (byte y = 0; y < yField; y++) { // loop through y column positions if (playField[xFieldLimit][y] > 0) { // GAME OVER! flooded = true; } } } // ...end flood check ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - GAME OVER! - Jewels have reached upper stack limit (flooded), the Flashing Jewel was crushed or the clock ran out in Time Trial // Update score, clear screen and variables, go back to splash screen // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// GameNowOver: {} if (flooded || (flashJewelCrushed && gameMode == 1) || timeIsUp || crushedByFloor) { // Wipe Field in dramatic fashion! // Sound Effect!!! execute_CMD(1, 0x0F, 1, 24); // Sweep Field Sound! (Folder 01, File 024.mp3) on 1 if (timeIsUp) { // if the countdown clock runs out... // Displays "Time Is Up!" byte text[] = {B00000000, B00001111, B00000100, B00010101, B00000100, B01101111, B00000000, B00000000, B00000000, B00010000, B01011011, B00000000, B00011100, B11100111, B00000010}; textOnLED(text, sizeof(text)); } else { // if not, either the jewels reached the top or the flash jewel was crushed in Flash Columns... if (flashJewelCrushed && gameMode == 1) { // Displays "You did it!" byte text[] = {B00000000, B00000000, B00000000, B00111011, B00011101, B00011100, B00000000, B00000000, B00111101, B00010000, B00111101, B00000000, B00010000, B00001111, B10100000}; textOnLED(text, sizeof(text)); } else { // this message pretty much applies to all other losing actions... // Displays "You LOSt!" byte text[] = {B00000000, B00000000, B00000000, B00111011, B00011101, B00011100, B00000000, B00000000, B00000000, B00000000, B00001110, B01111110, B01011011, B00001111, B10100000}; textOnLED(text, sizeof(text)); } } /// POST HIGH SCORE TO EEPROM and do Some resets... /// /// Record HIGH SCORE /// if (gameScore > highScore && gameMode == 0) { // (Original Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons highScore = gameScore; // the Level and numJewels are tied to the instance of High Score highLevel = gameLevel + gameLevelStart; highNumJewels = numJewels; // Update High Score, the associated High Level and High Jewel Count in EEPROM flash memory EEPROM.put(0, highScore); // 4 bytes EEPROM.put(4, highLevel); // 2 bytes EEPROM.put(6, highNumJewels); // 2 bytes } if (gameScore > highScoreTT && gameMode == 2 && timeIsUp) { // (Time Trial Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons // High Score ONLY to be kept if the time runs out and not if the jewels reach the top - failure has no glory! highScoreTT = gameScore; // the Level and numJewels are tied to the instance of High Score highLevelTT = gameLevel + gameLevelStart; highNumJewelsTT = numJewels; // Update High Score, the associated High Level and High Jewel Count in EEPROM flash memory EEPROM.put(24, highScoreTT); // 4 bytes EEPROM.put(28, highLevelTT); // 2 bytes EEPROM.put(30, highNumJewelsTT); // 2 bytes } if (gameScore > highScoreCC && gameMode == 3) { // (Crush Columns) Matches current score if not already higher, will be stored in EEPROM memory for later power-ons highScoreCC = gameScore; // the Level and numJewels are tied to the instance of High Score highLevelCC = gameLevel + gameLevelStart; highNumJewelsCC = numJewels; // Update High Score, the associated High Level and High Jewel Count in EEPROM flash memory EEPROM.put(32, highScoreCC); // 4 bytes EEPROM.put(36, highLevelCC); // 2 bytes EEPROM.put(38, highNumJewelsCC); // 2 bytes } if ((gameSeconds < bestSeconds || bestSeconds == 0) && gameSeconds > 0 && flashJewelCrushed && gameMode == 1) { // (Flash Columns) Matches current time if not already lower, can be stored in EEPROM memory for later power-ons bestSeconds = gameSeconds; // the Level and numJewels are tied to the instance of Fastest Time highLevelFC = gameLevel + gameLevelStart; highNumJewelsFC = numJewels; /* // disabled this feature due to an inherent scoring inequality for using different difficulty levels. // otherwise the following would update Fastest Time, the associated High Level and High Jewel Count in EEPROM flash memory // High Score for Flash Columns would then be tied to Original Columns with any gains from fastest time in Flash Columns only being kept within the game units current power-on condition EEPROM.update(18, bestSeconds); // 4 bytes EEPROM.update(20, highLevelFC); // 2 bytes EEPROM.update(22, highNumJewelsFC); // 2 bytes */ } if (gameMode == 1 && flooded) { gameSeconds = 0; } // with no capture of the flashing jewel in Flash Columns, we will omit the time - and thus will not show in the score board for the last game played gameLevelPrior = gameLevel + gameLevelStart; gameLevel = 0; gameLevelStart = 0; // Reset this temporary option if (rumbleMotor) { // Rumble Motor Option rumbleMotorLargeRequest = true; digitalWrite(rumbleMotorLargePin, HIGH); // Activate Large Rumble Motor } /// LED EFFECTS!!! /// SATURATION = 255; // Set to full color since the Magic Jewel is not used leds[NUM_LEDS-3] = CRGB(0,0,0); // Clear the preview LEDs leds[NUM_LEDS-2] = CRGB(0,0,0); leds[NUM_LEDS-1] = CRGB(0,0,0); FastLED.show(); // delay(300); // any needed delay to prolong the inevitable... // Sparkly Sweep... for (byte x = 0; x < (xField+8); x++) { // loop through x rows for (byte y = 0; y < yField; y++) { // loop through y columns for (byte z = 0; z < 8; z++) { // deviate steps from x if ((x - z) < xFieldLimit && (x - z) >= 0) { leds[ledIndex[x - z][y]] = CHSV(random(256),random((z * 32) + 32),random(256 - ((z * 32) + 32))); // Color is always random, saturation is least at its brightest } } } FastLED.show(); delay(70); //// The Speed of the Sparkly Sweep } if (rumbleMotor && rumbleMotorLargeRequest) { // Rumble Motor Option rumbleMotorLargeRequest = false; digitalWrite(rumbleMotorLargePin, LOW); // Deactivate Large Rumble Motor } /// Clear the LED Display and RESET the Game Play Field /// clearPlayField (); /// Ending Music /////////////////////////////////////////////////////////////////////////////////// if (gameMode == 0) { // if Original Columns was played... execute_CMD(2, 0x0F, 2, 19); // Long Game Over Song, (Folder 02, File 019.mp3) on 2 - "Concilation" } if (gameMode == 3) { // if Crush Columns was played... execute_CMD(2, 0x0F, 2, 3); // Long Game Over Song, (Folder 02, File 003.mp3) on 2 - "The End" } if (gameMode == 1) { // if Flash Columns was played... if (flashJewelCrushed) { execute_CMD(2, 0x0F, 2, 113); // Game Won Song 2, (Folder 02, File 113.mp3) on 2 - "Yes! Yes! Yes!" } else { execute_CMD(2, 0x0F, 2, 111); // Game Over Song 2, (Folder 02, File 111.mp3) on 2 - "Game Over" - Alt gameSeconds = 0; // Throw out the player's time if they drown! } } if (gameMode == 2) { // if Time Trial was played... if (timeIsUp) { execute_CMD(2, 0x0F, 2, 13); // Time's Up Song, (Folder 02, 013.mp3) on 2 - "Unused Jingle 1" } else { execute_CMD(2, 0x0F, 2, 2); // Game Over Song, (Folder 02, 002.mp3) on 2 - "Ashimai" } gameSeconds = 0; // Throw out the player's time } if (gameMode == 1) { flashJewelCrushed = false; } else { flashJewelCrushed = true; } // reset the flag for the appropriate game timeIsUp = false; // reset the flag crushedByFloor = false; // reset the flag stompDownFloor = 0; // reset the flag xFieldFloor = 0; // tear down the wall! difficultyFactor = oldDifficultyFactor; // reset the difficulty factor delay(2000); // a little pause gameOver = true; goto StartGame; // start game fresh - go back to Menu Mode } // ...end if Flooded check showNext = true; // Allow for preview set to appear in side window when xPos is in right position ///// The Magic Jewel may come to save the day! /////////////////////////////////////////////////////////////// /// Activate next occurance of the Magic Jewel according to chance /// magicJewel = false; // Reset flag - - Set these first just in case the below chance isn't met SATURATION = 255; // new pieces in full color if ((magicLevelAllotment && gameMode == 0) || gameMode == 3) { // Magic Level Allotment is given only once per level - ALSO: It's only available in Original Columns and Crush Columns if ((magicLevel > 0 && (gameLevel + gameLevelStart) >= magicLevel) || gameMode == 3) { // In Original Columns Magic Jewel can appear on established level (considering user-set level-up) and above, bring whenever in Crush Columns if (random(1001) > magicChance) { // RANDOM CHANCE - Magic Jewel doesn't come that often magicJewel = true; // Set flag to tell program that a Magic Jewel (takeover) is active magicLevelAllotment = false; // flag to restrict occurances to once per level - reset on level change playSet[0] = 9; // Not necessary to make a special jewel type/color, but elminates any confusion by any other procedure playSet[1] = 9; playSet[2] = 9; SATURATION = 0; // Set the base color of play pieces to white } } } ///-///-///-///-/// The following 3 closing brackets are from the - Game Play - section ///-///-///-///-/// } // ...end of detection to see if play piece is still in the air - end of taking care of when the play set lands } // ...end of timing check } // ...end of check to see if Settle Flag was raised // NOW GO BACK UP if the game is still going /////////////////////////////////////////////////// } /// End of VOID LOOP /// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - FUNCTIONS - // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// DFPLAYER MINI /// void execute_CMD(int Device, byte CMD, byte Par1, byte Par2) // Excecute the command and parameters for the DFPlayer Mini on the specified (Device) (SendOnly)SoftwareSerial { // Calculate the checksum (2 bytes) word checksum = -(Version_Byte + Command_Length + CMD + Acknowledge + Par1 + Par2); // Build the command line byte Command_line[10] = { Start_Byte, Version_Byte, Command_Length, CMD, Acknowledge, Par1, Par2, highByte(checksum), lowByte(checksum), End_Byte}; //Send the command line to the specified module if (Device == 1) { for (byte k=0; k<10; k++) { mySerial1.write( Command_line[k]); } } if (Device == 2) { for (byte k=0; k<10; k++) { mySerial2.write( Command_line[k]); } } } /// 7-SEGMENT LED DISPLAYS - GENERAL TEXT /// void textOnLED(byte text[], int sizeoftext) { // Passes pre-set text (by bytes) and sends them to the two 8 x 8, 7-Segment LED display modules // lc.clearDisplay(0); // lc.clearDisplay(1); lc.shutdown(0,false); lc.shutdown(1,false); for (byte i = 0; i < 8; i++) { // Read through the text array and transfer the led matrix description bytes to the LED display command if (8 - i <= sizeoftext) { // We can save on text array size by only displaying the what is needed lc.setRow(0,i, text[7 - i]); } else { lc.setRow(0,i, B00000000); } if (16 - i <= sizeoftext) { lc.setRow(1,i, text[15 - i]); } else { lc.setRow(1,i, B00000000); } } } /// 7-SEGMENT LED DISPLAYS - RIGHT JUSTIFIED, FIRST DISPLAY /// void scoreBoardLED(unsigned long gameScore) { lc.shutdown(0,false); lc.setDigit(0,0,gameScore %10,false); if (gameScore > 9) { lc.setDigit(0,1,gameScore/10 %10,false); } else { lc.setChar(0,1,' ',false); } if (gameScore > 99) { lc.setDigit(0,2,gameScore/100 %10,false); } else { lc.setChar(0,2,' ',false); } if (gameScore > 999) { lc.setDigit(0,3,gameScore/1000 %10,false); } else { lc.setChar(0,3,' ',false); } if (gameScore > 9999) { lc.setDigit(0,4,gameScore/10000 %10,false); } else { lc.setChar(0,4,' ',false); } if (gameScore > 99999) { lc.setDigit(0,5,gameScore/100000 %10,false); } else { lc.setChar(0,5,' ',false); } if (gameScore > 999999) { lc.setDigit(0,6,gameScore/1000000 %10,false); } else { lc.setChar(0,6,' ',false); } if (gameScore > 9999999) { lc.setDigit(0,7,gameScore/10000000 %10,false); } else { lc.setChar(0,7,' ',false); } } /// 7-SEGMENT LED DISPLAYS - LEFT JUSTIFIED, SECOND DISPLAY /// void gameLevelLED(int gameLevel) { if (gameLevel > 99) { // 3 digit placement from the left lc.shutdown(1,false); lc.setDigit(1,5,gameLevel %10,false); lc.setDigit(1,6,gameLevel/10 %10,false); lc.setDigit(1,7,gameLevel/100 %10,false); } if (gameLevel > 9 && gameLevel <= 99) { // 2 digit placement from the left lc.shutdown(1,false); lc.setChar(1,5,' ',false); lc.setDigit(1,6,gameLevel %10,false); lc.setDigit(1,7,gameLevel/10 %10,false); } if (gameLevel <= 9) { // 1 digit placement from the left lc.shutdown(1,false); lc.setChar(1,5,' ',false); lc.setChar(1,6,' ',false); lc.setDigit(1,7,gameLevel %10,false); } } /// 7-SEGMENT LED DISPLAYS - RIGHT JUSTIFIED, SECOND DISPLAY /// void numJewelsLED(int numJewels) { lc.shutdown(1,false); lc.setDigit(1,0,numJewels %10,false); if (numJewels > 9) { lc.setDigit(1,1,numJewels/10 %10,false); } else { lc.setChar(1,1,' ',false); } if (numJewels > 99) { lc.setDigit(1,2,numJewels/100 %10,false); } else { lc.setChar(1,2,' ',false); } if (numJewels > 999) { lc.setDigit(1,3,numJewels/1000 %10,false); } else { lc.setChar(1,3,' ',false); } } /// 7-SEGMENT LED DISPLAYS - RIGHT JUSTIFIED, SPECIFIED DISPLAY /// void selectLED(byte deviceLED, int numDisplay) { lc.shutdown(deviceLED,false); lc.setDigit(deviceLED,0,numDisplay %10,false); if (numDisplay > 9) { lc.setDigit(deviceLED,1,numDisplay/10 %10,false); } else { lc.setChar(deviceLED,1,' ',false); } } /// 7-SEGMENT LED DISPLAYS - LEFT JUSTIFIED, FIRST DISPLAY /// void gameTimeLED(int gameSecondsLED, bool flashColonLED) { // gameSecondsLED is in total seconds, so we need to derive the minute and carry-over seconds - MM:SS, MMM:SS if necessary byte placeOverMinutes = 1; lc.shutdown(1,false); if (((gameSecondsLED / 60)/100 %10) > 0) { placeOverMinutes = 0; lc.setDigit(0,7,(gameSecondsLED / 60)/100 %10, false); } lc.setDigit(0,6 + placeOverMinutes,(gameSecondsLED / 60)/10 %10, false); lc.setDigit(0,5 + placeOverMinutes,(gameSecondsLED / 60) %10, flashColonLED); lc.setDigit(0,4 + placeOverMinutes,(gameSecondsLED - ((gameSecondsLED / 60) * 60))/10 %10, false); lc.setDigit(0,3 + placeOverMinutes,(gameSecondsLED - ((gameSecondsLED / 60) * 60)) %10, false); } /// NAVIGATION BUTTON DETECT for MENUS /// void menuButtonDetect() { if (digitalRead(ArrangeButtonPin) == LOW) { // Is the Up button being pressed? // Then keep in a holding pattern until it is not, also if the Down button is pressed after // Hold while either is down while (digitalRead(ArrangeButtonPin) == LOW || digitalRead(DownButtonPin) == LOW) { if (digitalRead(ArrangeButtonPin) == LOW && digitalRead(DownButtonPin) == LOW) { bothUpDownButtons = true; } } if (!bothUpDownButtons) { upButton = true; } } else { upButton = false; } if (digitalRead(DownButtonPin) == LOW) { // Is the Down button being pressed? // Then keep in a holding pattern until it is not, also if the Up button is pressed after // Hold while either is down while (digitalRead(ArrangeButtonPin) == LOW || digitalRead(DownButtonPin) == LOW) { if (digitalRead(ArrangeButtonPin) == LOW && digitalRead(DownButtonPin) == LOW) { bothUpDownButtons = true; } } if (!bothUpDownButtons) { downButton = true; } } else { downButton = false; } if (digitalRead(LeftButtonPin) == LOW) { // Is the Left button being pressed? // Then keep in a holding pattern until it is not, also if the Right button is pressed after // Hold while either is down while (digitalRead(LeftButtonPin) == LOW || digitalRead(RightButtonPin) == LOW) { if (digitalRead(LeftButtonPin) == LOW && digitalRead(RightButtonPin) == LOW) { bothLeftRightButtons = true; } } if (!bothLeftRightButtons) { leftButton = true; } } else { leftButton = false; } if (digitalRead(RightButtonPin) == LOW) { // Is the Left button being pressed? // Then keep in a holding pattern until it is not, also if the Left button is pressed after // Hold while either is down while (digitalRead(LeftButtonPin) == LOW || digitalRead(RightButtonPin) == LOW) { if (digitalRead(LeftButtonPin) == LOW && digitalRead(RightButtonPin) == LOW) { bothLeftRightButtons = true; } } if (!bothLeftRightButtons) { rightButton = true; } } else { rightButton = false; } } /// CHECK GAME BOARD CLOCK AND UPDATE DISPLAY /// void gameClock () { timeTrack = millis(); // META-TIMING Reference Update from the AVR - - - THIS UPDATE IS CRUCIAL TO GAME PLAY if (gameMode == 1) { // Game Clock - if we are playing Flash Columns... gameSeconds = (timeTrack - gameStartReferencePoint) / 1000; // establish general accumulated seconds based on deviation from game start // determine state of display colon // if ( flashColon == true && (timeTrack - gameStartReferencePoint) - (((timeTrack - gameStartReferencePoint) / 1000) * 1000) < 500 ) { // DISPLAY GAME TIME - Write to the Left part of the Top LED Numerical Display // gameTimeLED(gameSeconds, flashColon); // flashColon - 'true' is the decimal flashColon = false; } if ( flashColon == false && (timeTrack - gameStartReferencePoint) - (((timeTrack - gameStartReferencePoint) / 1000) * 1000) >= 500 ) { // DISPLAY GAME TIME - Write to the Left part of the Top LED Numerical Display // gameTimeLED(gameSeconds, flashColon); // flashColon - 'false' is no decimal flashColon = true; } } if (gameMode == 2) { // Game Clock - if we are playing Time Trial Columns... gameSeconds = 180 - (timeTrack - gameStartReferencePoint) / 1000; // establish general remaining seconds based on deviation from game start if ( timeTrack - gameStartReferencePoint >= 180000 ) { // if 3 (actual, not virtual) minutes have passed... timeIsUp = true; // signify when the clock has run out } // determine state of display colon // if ( flashColon == true && (timeTrack - gameStartReferencePoint) - (((timeTrack - gameStartReferencePoint) / 1000) * 1000) < 500 ) { // DISPLAY GAME TIME - Write to the Left part of the Top LED Numerical Display // gameTimeLED(gameSeconds, flashColon); // flashColon - 'true' is the decimal flashColon = false; } if ( flashColon == false && (timeTrack - gameStartReferencePoint) - (((timeTrack - gameStartReferencePoint) / 1000) * 1000) >= 500 ) { // DISPLAY GAME TIME - Write to the Left part of the Top LED Numerical Display // gameTimeLED(gameSeconds, flashColon); // flashColon - 'false' is no decimal flashColon = true; } } } /// CLEAR PLAY FIELD /// void clearPlayField () { for (byte y = 0; y < yField; y++) { // loop through y columns for (byte x = 0; x < xField; x++) { // loop through x rows playField[x][y] = 0; if (x < xFieldLimit) { leds[ledIndex[x][y]] = CRGB(0,0,0); } } } FastLED.show(); } /// EMERGENCY REDUCED POWER /// void reducePowerForLowBattery () { // Sound Effect!!! execute_CMD(1, 0x0F, 1, 16); // Down Power Alert Sound, "Wild Shellfish" (Folder 01, File 016.mp3) on 1 // LOWER Dev 1 Volume Dev1Volume = constrain(Dev1Volume, 0, 15); // The Sound Effects Volume will now be reduced to 15 execute_CMD(1, 0x06, 0, Dev1Volume); // Set the volume on 1 (0x00~0x30) // LOWER Dev 2 Volume execute_CMD(2, 0x06, 0, 0); // Set the volume on 2 (0x00~0x30) Music will be shut off // Set LED Numerical Displays to a very low brightness... lc.setIntensity(0,1); // Set brightness of first 7-Seg LED display lc.setIntensity(1,1); // Set brightness of second 7-Seg LED display FastLED.setBrightness(50); // Set brightness for the addressable LEDs. 50-255 is a usable range with color consistency issues noted at low end. User experimental FastLED.show(); // Just in case this is needed for update // As these values are hard-set instead of by variables, user-choice settings will not be affected rumbleMotor = false; // May be set as permanent if user plays another game, but this is a consumer of power } /// DISPLAY RECHARGE BATTERY TEXT /// void displayRechargeBatteryText () { // Displays the text "RECHARGE battery!" // Displays "RECHARGE battery!" byte text[] = {B11110111, B01001111, B01001110, B00110111, B01110111, B11110111, B01011110, B01001111, B00011111, B01111101, B00001111, B00001111, B01101111, B00000101, B00111011, B10100000}; // Recharge Battery! textOnLED(text, sizeof(text)); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // - REFERENCE - // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// AUDIO NUMBERING LISTS for SD CARDS /// /* Sound Effect / Background Music List - as indexed and arranged in the SD card set: * ================================================================================== * * [Folder 01] * * Basic Sound Effects * * 001.mp3 Original Jewel Crush Sound * 002.mp3 Noise Smash Sound * 003.mp3 Selection Sound * 004.mp3 Original Landing Collision Sound * 005.mp3 Level-Up or Bonus Jingling Sound * 006.mp3 Side Collision Sound (Laser Blaster Thud) * 007.mp3 Bonus Warbling Sound * 008.mp3 Space Ship Take Off Sound (unused) * 009.mp3 Landing Sound (unused) * 010.mp3 Arrange Sound * 011.mp3 Punch or Skip Sound (unused) * 012.mp3 Frog Hop (unused) * 013.mp3 Ricochet (unused) * 014.mp3 Machine Gun or Hellicopter Sound (unused) * 015.mp3 Coin or Points Counting Sound (unused) * 016.mp3 Wild Shellfish * 017.mp3 The Opening Bell * * Mono Default Sound Effects * * 020.mp3 Jewel Crush Sound - 1st Occurance * 021.mp3 Jewel Crush Sound - 2nd Occurance * 022.mp3 Jewel Crush Sound - 3rd Occurance * 023.mp3 Jewel Crush Sound - 4th Occurance and on... * 024.mp3 Play Field Obliteration - Game Over * * Stereo Positional Sound Effects * * 069.mp3 Side Collision Sound [RIGHT] * 070.mp3 Side Collision Sound [...] * 071.mp3 Side Collision Sound [...] * 072.mp3 Side Collision Sound [...] * 073.mp3 Side Collision Sound [...] * 074.mp3 Side Collision Sound [LEFT] * 075.mp3 Arrange Sound [RIGHT] * 076.mp3 Arrange Sound [...] * 077.mp3 Arrange Sound [...] * 078.mp3 Arrange Sound [...] * 079.mp3 Arrange Sound [...] * 080.mp3 Arrange Sound [LEFT] * 081.mp3 Landing Collision Sound [RIGHT] * 082.mp3 Landing Collision Sound [...] * 083.mp3 Landing Collision Sound [...] * 084.mp3 Landing Collision Sound [...] * 085.mp3 Landing Collision Sound [...] * 086.mp3 Landing Collision Sound [LEFT] * 087.mp3 Jewel Crush Sound - 1st Occurance [RIGHT] * 088.mp3 Jewel Crush Sound - 1st Occurance [...] * 089.mp3 Jewel Crush Sound - 1st Occurance [...] * 090.mp3 Jewel Crush Sound - 1st Occurance [...] * 091.mp3 Jewel Crush Sound - 1st Occurance [...] * 092.mp3 Jewel Crush Sound - 1st Occurance [LEFT] * 093.mp3 Jewel Crush Sound - 2nd Occurance [RIGHT] * 094.mp3 Jewel Crush Sound - 2nd Occurance [...] * 095.mp3 Jewel Crush Sound - 2nd Occurance [...] * 096.mp3 Jewel Crush Sound - 2nd Occurance [...] * 097.mp3 Jewel Crush Sound - 2nd Occurance [...] * 098.mp3 Jewel Crush Sound - 2nd Occurance [LEFT] * 099.mp3 Jewel Crush Sound - 3rd Occurance [RIGHT] * 100.mp3 Jewel Crush Sound - 3rd Occurance [...] * 101.mp3 Jewel Crush Sound - 3rd Occurance [...] * 102.mp3 Jewel Crush Sound - 3rd Occurance [...] * 103.mp3 Jewel Crush Sound - 3rd Occurance [...] * 104.mp3 Jewel Crush Sound - 3rd Occurance [LEFT] * 105.mp3 Jewel Crush Sound - 4th Occurance [RIGHT] * 106.mp3 Jewel Crush Sound - 4th Occurance [...] * 107.mp3 Jewel Crush Sound - 4th Occurance [...] * 108.mp3 Jewel Crush Sound - 4th Occurance [...] * 109.mp3 Jewel Crush Sound - 4th Occurance [...] * 110.mp3 Jewel Crush Sound - 4th Occurance [LEFT] * * * [Folder 02] * * Default Columns Event Music * * 001.mp3 "Kuroto's Origin" * 002.mp3 "Ashimai" * 003.mp3 "The End" * 004.mp3 "Start Stage" * 005.mp3 "Victory!" * 006.mp3 "Introductory Theme" * 007.mp3 "Yes! Yes!" * 008.mp3 "Unfinished Roll" * 009.mp3 "Pokasuka Beginning" * 010.mp3 "Mummy's Awakening" * 011.mp3 "Snowman" * 012.mp3 "Title Screen" - Quick Drum Procession * 013.mp3 "Unused Jingle 1" * 014.mp3 "Unused Jingle 2" * 015.mp3 "Unused Jingle 3" * 016.mp3 "Filthy" Opening (Lower Volume) * 017.mp3 "Clotho" (Lower Volume) * 018.mp3 "Quagmire" Ending - Alt * 019.mp3 "Concilation" Ending * 020.mp3 "Enigma" * 021.mp3 "Morning" - Extended Drum Procession * 022.mp3 "Danger I" * 023.mp3 "Danger II" * 024.mp3 "Danger III" * 025.mp3 "Filthy" Opening * 026.mp3 "Quagmire" Ending - Alt * 027.mp3 "Quagmire" Ending - Alt (A copy) * * Columns Background Music * * 028.mp3 Acquired Music from Columns I, II, II and more, * . with remixes and original recordings. * . - - - Selected by random in sketch * . * 108.mp3 * * Special Columns Event Music * * 109.mp3 "Select" * 110.mp3 "Name Entry" * 111.mp3 "Game Over" jingle - Alt * 112.mp3 "Boy Wonder, Name Entry" Jingle * 113.mp3 "Yes! Yes! Yes!" Jingle * * Extended Columns Background Music * * 114.mp3 Extended Acquired Music Set from Columns I, II, II and more, * . with remixes and original recordings. * . - - - Selected by random in sketch * . * 130.mp3 * * * [Folder 03] * * Special Alternate Music * * 001.mp3 Amiga Tracker, ChipTune and KeyGen Music * . - - - Selected by random in sketch * . Played during Flash Columns * . and after level 10 in Original Columns * . * 255.mp3 * * [Folder 04] * * Timed Music * * 001.mp3 Columns Music at 3:00 min in duration with progressing speed and countdown timer sound * . for Time Trial Columns * . * 015.mp3 * * * */