/*
Riichi Digital Compass and Score Keeper
Designed and code written by Tanner Marchant
Basic function of this device is to keep track of points,
table wind, table round, and player winds.
Upon starting the program the user will be prompted for
number of players and whether they want to play east only,
or hanchan.
Keypad numbers function as such, the '*' key acts as a backspace,
the '#' key functions as enter, and A B C D will be relabelled as
East, South, West, and North respectivley.
Control Scheme
Calling Riichi
Wind, #
Calling Ron
Winning Wind, Losing Wind, #, Han, #, Fu, #
Calling Tsumo
Winning Wind, Winning Wind, #, Han, #, Fu, #
Ryuukyokuu
Double tap 0, Toggle Winds in tenpai, #
End conditions for the game are monitered and trigger automatically
showing final score and rank of the players. Score is calculated in
the standard manner with Uma of [30,10,-10,-30] for 4 players and
[15,0,-15] for sanma.
*/
// Necesarry libraries
#include <Wire.h>
#include <Keypad.h>
#include <LiquidCrystal_I2C.h>
// Keypad Row and Column sizes
const byte ROWS = 4;
const byte COLS = 4;
//Array to represent the keys
char keys[ROWS][COLS] =
{
{'1','2','3','E'},
{'4','5','6','S'},
{'7','8','9','W'},
{'*','0','#','N'}
};
// Connect keypad to arduino pins
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
// Create keypad object
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
// Create LCD displays
LiquidCrystal_I2C lcd[4] ={
LiquidCrystal_I2C(0x27, 16, 2), // Seat 1 at address 0x27
LiquidCrystal_I2C(0x26, 16, 2), // Seat 2 at address 0x26
LiquidCrystal_I2C(0x25, 16, 2), // Seat 3 at address 0x25
LiquidCrystal_I2C(0x24, 16, 2) // Seat 4 at address 0x24
};
// Initialize Key read and user input variables
char key;
char lastKey;
char prevKey;
byte numericalKey;
boolean entryComplete;
boolean entryExist;
long val = 0;
int delCount = 0;
int inputCount = 0;
// Tablewide Variables
byte playerCount = 0;
byte roundCount = 1;
byte tableCount = 0;
byte gameLength = 0;
char tableWind[4] = {'E','S','W','N'};
boolean gameEnd;
int honbaCount = 0;
int potValue = 0;
int han = 0;
int fu = 0;
boolean tsumo = false;
long pointsTransferred = 0;
int uma[4];
long pointThresh;
long initPoints;
// Player Variables
// Arrays are used for both the player seats and the winds.
long playerPoints[4];
String wind[4] = {"East","South","West","North"};
byte seat[4];
byte rank[4] = {0,0,0,0};
boolean inRiichi[4] = {0,0,0,0};
byte priorityPlayer;
byte dealInPlayer;
byte currentWind;
byte priorityWind;
boolean inTenpai[4] = {0,0,0,0};
void setup() // Initial Setup Loop
{
for(int i=0; i<4; i++) // Initialize screens & turn them on
{
lcd[i].init();
lcd[i].backlight();
}
}
void loop() // Main loop
{
gameEnd = false;
for(int i = 0; i<4; i++) // Display each seat wind for reference
{
seat[i] = i;
lcd[i].clear();
lcd[i].print(wind[seat[i]]);
}
delay(1500);
// Ask for number of players
lcd[0].setCursor(0, 1);
lcd[0].print("Players?");
lcd[0].setCursor(9,1);
lcd[0].blink();
// Get user response
do
{
entryComplete = false;
while(entryComplete == false)
{
readKeypad();
if((key == '3' || key == '4') && inputCount == 0) // Limit Keypad use to 3,4,* or #
{
dispInput(lcd[0]);
}
else if(key == '*' || key == '#')
{
dispInput(lcd[0]);
}
else
{
key = NO_KEY;
}
}
playerCount = prevKey - 48; // converts ASCII number to actual digit
}while(playerCount != 3 && playerCount != 4); //Continue loop until input is 3 or 4
lcd[0].noBlink();
delay(1500);
// East only or Hanchan?
lcd[0].setCursor(0, 0);
lcd[0].print("Game Length? ");
lcd[0].setCursor(0,1);
lcd[0].print("(E or S) ");
lcd[0].setCursor(9,1);
lcd[0].blink();
entryComplete = false;
entryExist = false;
// Get response
while(entryComplete == false || entryExist == false)
{
readKeypad();
if(inputCount == 0)
{
if (key == 'E' && prevKey != 'E')
{
gameLength = 0;
entryExist = true;
dispInput(lcd[0]);
}
if (key == 'S' && prevKey != 'S')
{
gameLength = 1;
entryExist = true;
dispInput(lcd[0]);
}
}
if(key == '*')
{
entryExist = false;
dispInput(lcd[0]);
}
}
lcd[0].noBlink();
if(playerCount == 3) // Three player SANMA game
{
lcd[3].clear(); // Disable North screen
lcd[3].noBacklight();
initPoints = 35000;
pointThresh = 40000;
seat[0] = 0; // E
seat[1] = 1; // S
seat[2] = 2; // W
uma[0] = 15; // Set uma
uma[1] = 0;
uma[2] = -15;
}
if(playerCount == 4) // Regular 4 player game
{
lcd[3].backlight();
initPoints = 25000;
pointThresh = 30000;
seat[0] = 0; // E
seat[1] = 1; // S
seat[2] = 2; // W
seat[3] = 3; // N
uma[0] = 30; // Set uma
uma[1] = 10;
uma[2] = -10;
uma[3] = -30;
}
for(int i = 0; i < playerCount; i++) // Set Points to starting amount
{
playerPoints[i] = initPoints;
}
updateScreens();
while(gameEnd == false) // Main game loop
{
readKeypad();
// Score or Riichi Event
if(key == 'E' || key == 'S' || key == 'W' || (key == 'N' && playerCount == 4))
{
wind2Digit(); // Convert wind input to index number
priorityWind = currentWind;
for(int i=0; i<playerCount; i++) // Locate player based on wind
{
if(seat[i] == priorityWind)
{
priorityPlayer = i; // Mark winning player
}
}
entryComplete = false;
lcd[priorityPlayer].setCursor(6,0);
lcd[priorityPlayer].blink(); // Show which player has priority
while(entryComplete == false) // Await more input
{
readKeypad();
// Rescind Entry
if (key == '*')
{
entryComplete = true;
}
// Riichi Event
if(key == '#' && inRiichi[priorityPlayer] == false) // Ensure they can't call riichi twice
{
playerPoints[priorityPlayer] = playerPoints[priorityPlayer] -1000; // Deduct Riichi bet
potValue = potValue + 1000; // Add bet to the pot
inRiichi[priorityPlayer] = true; // indicate player is in riichi
updateScreens(); // update the lcds
}
// Ron or Tsumo Event
if(key == 'E' || key == 'S' || key == 'W' || (key == 'N' && playerCount == 4))
{
wind2Digit();
// Mark as Ron or Tsumo
if(currentWind == priorityWind) // Tsumo event display
{
lcd[priorityPlayer].setCursor(6,0);
lcd[priorityPlayer].print("Tsumo ");
tsumo = true;
}
else // Ron event display
{
lcd[priorityPlayer].setCursor(6,0);
lcd[priorityPlayer].print("Ron=>");
lcd[priorityPlayer].print(key);
tsumo = false;
for(int i=0; i<playerCount; i++) // Locate player based on wind
{
if(seat[i] == currentWind)
{
dealInPlayer = i; // Identify loser
}
}
}
while(key != '#' && key != '*') // Only check for entry or backspace
{
readKeypad();
if(key == '#') // Confirm Entry & Score
{
score();
passPoints();
updateScreens();
delay(1500);
if(seat[priorityPlayer] != 0) // No honba added
{
honbaCount = 0;
updateWinds();
updateScreens();
}
else // Add honba
{
honbaCount ++;
updateScreens();
}
}
if(key == '*') // Rescind Entry
{
lcd[priorityPlayer].setCursor(6,0);
lcd[priorityPlayer].print(" ");
lcd[priorityPlayer].setCursor(6,0);
}
}
}
}
}
lcd[priorityPlayer].noBlink();
// Exhaustive Draw
if(key == '0' && prevKey == '0') // Triggers when 0 is double tapped
{
for(int i = 0; i < playerCount; i++) // Display ryuukyoku on each screen
{
lcd[i].setCursor(0,1);
lcd[i].print(" ");
lcd[i].setCursor(0,1);
lcd[i].print(" Ryuukyoku? ");
}
while(key != '*' && key != '#') // Confirm or rescind
{
readKeypad();
if(key == '#') // Confirm
{
for(int i = 0; i < playerCount; i++) // Display default player status as Noten
{
lcd[i].setCursor(12,1);
lcd[i].print(" ");
lcd[i].setCursor(3,1);
lcd[i].print(" Noten ");
}
key = NO_KEY;
while(key != '#') // Wait to enter tenpai players
{
readKeypad();
if(key == 'E' || key == 'S' || key == 'W' || (key == 'N' && playerCount == 4)) // When wind is pressed
{
wind2Digit();
inTenpai[currentWind] = !inTenpai[currentWind]; // Toggle tenpai
for(int i=0; i<playerCount; i++) // Locate player based on wind
{
if(seat[i] == currentWind)
{
priorityPlayer = i; // Mark tenpai player
}
}
lcd[priorityPlayer].setCursor(0,1); // Print tenpai/noten status
lcd[priorityPlayer].print(" ");
lcd[priorityPlayer].setCursor(5,1);
if(inTenpai[currentWind] == true)
{
lcd[priorityPlayer].print("Tenpai");
}
else
{
lcd[priorityPlayer].print("Noten ");
}
}
if(key == '#') // Confirm
{
updateScreens();
delay(1500);
for(int i = 0; i<playerCount; i++) // Count # of players in Tenpai
{
if(inTenpai[i] == 1)
{
val ++;
}
}
for(int i = 0; i<playerCount; i++) // Divvy up Points
{
if(inTenpai[seat[i]] == true && val != playerCount) // Give points to tenpai players
{
playerPoints[i] += (3000 / val);
}
else if (val != playerCount) // Take points from those not in tenpai
{
playerPoints[i] -= (3000 / (playerCount - val));
}
}
updateScreens();
delay(1500);
if(inTenpai[0] == 0) // If dealer is not in tenpai seat winds change
{
updateWinds();
}
for(int i = 0; i<playerCount; i++) // Reset Tenpai Status
{
inTenpai[i] = false;
}
honbaCount ++; // Add a honba counter
updateScreens();
}
}
}
if(key == '*') // Rescind
{
updateScreens();
}
}
}
// Endgame Conditions
for(int i = 0; i<playerCount; i++)
{
if(playerPoints[i] < 0) // If a player falls below 0 points, trigger endgame
{
gameEnd = true;
}
}
if(tableCount > gameLength) // if table wind counter exceeds set game length
{
for(int i=0; i<playerCount; i++) // Locate dealer
{
if(seat[i] == 0)
{
priorityPlayer = i; // Mark dealer
}
}
rankPlayers();
if(honbaCount == 0) // Given no dealer repeat
{
for(int i = 0; i<playerCount; i++)
{
if(playerPoints[i] > pointThresh) // Check if players have met minimum point req to win
{
gameEnd = true;
}
}
if(tableCount > (gameLength + 1)) // if all are under the point requirement, allow one more table round
{
gameEnd = true;
}
}
else if (rank[priorityPlayer] == 1) //If dealer in first place with a repeat, trigger endgame
{
gameEnd = true;
}
}
if(gameEnd == true) // Endgame actions
{
rankPlayers(); // Determine Ranks
for(int i=0; i<playerCount; i++)
{
// Display Rank
lcd[i].setCursor(7,0);
lcd[i].print("Rank ");
lcd[i].setCursor(12,0);
lcd[i].print(rank[i]);
// Determine Score and Uma
val = 0;
val = playerPoints[i] - initPoints;
if (val % 1000 > 500)
{
val = (val + 500) / 1000;
}
else if (val % 1000 < -500)
{
val = (val - 500) / 1000;
}
else
{
val = val / 1000;
}
val += uma[rank[i]-1];
// Display Score
lcd[i].setCursor(7,1);
lcd[i].print("Score ");
lcd[i].setCursor(13,1);
lcd[i].print(val);
}
// Reset game counters and variables
honbaCount = 0;
roundCount = 1;
tableCount = 0;
entryComplete = false;
key = NO_KEY;
while(entryComplete == false)
{
readKeypad();
}
}
}
}
void rankPlayers() // Function that ranks players by score
{
for (int i = 0; i < playerCount; i++)
{
rank[i] = 1; // Rank initially assumed at highest 1
for (int j = 0; j < playerCount; j++)
{
if (
(playerPoints[j] > playerPoints[i]) || // For each player with more points, lower current players rank
(playerPoints[j] == playerPoints[i] && seat[j] < seat[i]) // If two players are tied for points look at seat order
) {
rank[i]++;
}
}
}
}
void roundup() // Round an integer to the nearest 100
{
val = ((val + 99) / 100) * 100;
}
void passPoints() // Calculate points and update player totals
{
// Calculate basic points
if((fu % 10) != 0 && fu != 25) // Round up fu to nearest 10 except for Chiitoitsu
{
fu = (fu / 10) + 10;
}
if(han > 12) { val = 8000; } else // Yakuman
if(han > 10) { val = 6000; } else // Sanbaiman
if(han > 7) { val = 4000;} else // Baiman
if(han > 5) { val = 3000;} else // Haneman
if(han == 5) { val = 2000;} else // Mangan
if(han < 5) // Han and Fu
{
val = fu * pow(2,(2 + han));
if(val > 2000) // Cap lower hands basic points at 2000
{
val = 2000;
}
}
// Multipliers
if(seat[priorityPlayer] == 0) // Dealer Win
{
if(tsumo == true) // Tsumo x2 mult
{
val = val * 2;
}
else // Ron x6 mult
{
val = val * 6;
}
}
else if (tsumo == false) // Nondealer Ron x4 mult
{
val = val * 4;
}
// Subtract points
for(int i = 0; i<playerCount; i++) // Evaluate each seat
{
if(i != priorityPlayer) // Don't deduct points from winner
{
if(tsumo == true) // Tsumo
{
if(seat[i] == 0)
{
playerPoints[i] += - ((((2 * val) + 99) / 100) * 100) - (honbaCount * 100); // Dealer pays more for tsumo
pointsTransferred += ((((2 * val) + 99) / 100) * 100) + (honbaCount * 100); // Keep track of points deducted
}
else
{
roundup();
playerPoints[i] += - val - (honbaCount * 100); // Non dealer payment
pointsTransferred += val + (honbaCount * 100); // Keep track of points deducted
}
}else if(i == dealInPlayer) // Ron
{
roundup();
playerPoints[dealInPlayer] -= val + (honbaCount * 300); // Deduct points
pointsTransferred += val + (honbaCount * 300); // Keep track of points deducted
}
}
}
// Add Points
playerPoints[priorityPlayer] += pointsTransferred + potValue;
// Reset Counters
pointsTransferred = 0;
potValue = 0;
for(int i = 0; i<playerCount; i++)
{
inRiichi[i] = 0;
}
}
void score() // Allow players to enter their score
{
entryComplete = false;
// Display and ask for Han
lcd[priorityPlayer].setCursor(0,1);
lcd[priorityPlayer].print(" ");
lcd[priorityPlayer].setCursor(2,1);
lcd[priorityPlayer].print("Han ");
lcd[priorityPlayer].blink();
// Get response
while(entryComplete == false)
{
readKeypad();
dispInput(lcd[priorityPlayer]);
if(key != '#')
{
han = val;
}
if(key == '#' && han == 0) // If user doesn't input anything
{
entryComplete = false;
lcd[priorityPlayer].setCursor(0,1);
lcd[priorityPlayer].print(" Invalid ");
delay(1000);
lcd[priorityPlayer].setCursor(0,1);
lcd[priorityPlayer].print(" ");
lcd[priorityPlayer].setCursor(2,1);
lcd[priorityPlayer].print("Han ");
}
}
if(han < 5) // Ask for fu for low scoring hands
{
lcd[priorityPlayer].print(" Fu ");
entryComplete = false;
// Get response
while(entryComplete == false)
{
readKeypad();
dispInput(lcd[priorityPlayer]);
if(key != '#')
{
fu = val;
}
if(key == '#' && fu < 20) // Do not allow Fu less than 20
{
entryComplete = false;
lcd[priorityPlayer].setCursor(0,1);
lcd[priorityPlayer].print(" Invalid ");
delay(1000);
lcd[priorityPlayer].setCursor(0,1);
lcd[priorityPlayer].print(" ");
lcd[priorityPlayer].setCursor(2,1);
lcd[priorityPlayer].print("Han ");
lcd[priorityPlayer].print(han);
lcd[priorityPlayer].print(" Fu ");
}
}
}
lcd[priorityPlayer].noBlink();
delay(1500);
}
void wind2Digit() // Reads wind key input as a index digit
{
switch(key)
{
case 'E':
currentWind = 0;
break;
case 'S':
currentWind = 1;
break;
case 'W':
currentWind = 2;
break;
case 'N':
currentWind = 3;
break;
}
}
void updateWinds() // Shifts the winds one seat counterclockwise and updates the round
{
for(int i = 0; i < playerCount; i++) // For each seat
{
seat[i] = (seat[i] + (playerCount - 1)) % playerCount; // Shift wind position by 1
}
if(roundCount < playerCount) // Up the round counter
{
roundCount ++;
}
else // Unless you are in round 4, then reset the counter and change the table wind
{
roundCount = 1;
tableCount ++;
}
}
void updateScreens() // Re-print the player screens with updated scores
{
for(int i=0; i<playerCount; i++)
{
lcd[i].clear(); // Clear screen
lcd[i].setCursor(0,0);
lcd[i].print(wind[seat[i]]); // Show Wind
lcd[i].setCursor(14,0);
lcd[i].print(tableWind[tableCount]); // Show Round
lcd[i].print(roundCount);
lcd[i].setCursor(0,1);
lcd[i].print(playerPoints[i]); // Show Points
if(honbaCount != 0)
{
lcd[i].setCursor(9,1);
lcd[i].print("Honba "); // Display Honba count
lcd[i].print(honbaCount);
}
if(inRiichi[i])
{
lcd[i].setCursor(6,0); // Display Riichi Status
lcd[i].print("Riichi");
}
}
}
void readKeypad() //Key input function
{
key = keypad.getKey(); // Read keypad
if(key != NO_KEY) // If a key has been pressed
{
prevKey = lastKey;
lastKey = key;
}
if(key == '#') // if user presses # (enter)
{
entryComplete = true; //Flag completed input
inputCount = 0; // reset counts
delCount = 0;
val = 0;
lastKey = NO_KEY;
}
if(key == '*') // If user presses * (delete)
{
val = val / 10; // Round down the running total by 10s place
}
if(key >= '0' && key <= '9') // If user presses key 0 through 9
{
numericalKey = key - 48; // Convert character to digit
val = val * 10; // increase magnitude of previous input by 10
val = val + numericalKey; // Add the number to the running total
}
}
void dispInput(LiquidCrystal_I2C &seat) // Function for printing keypad input to lcd
{
if(key == lastKey && key != NO_KEY) // If last keypress exists,
{
if(lastKey == '*') // And is delete button,
{
if(inputCount > 0) // Keeps user from rewriting screen prior to input location.
{
seat.rightToLeft(); // clear the last printed value and reset the cursor
seat.print(" ");
seat.leftToRight();
seat.print(" ");
seat.rightToLeft();
seat.print(" ");
seat.leftToRight();
inputCount --; // increment the count of delete events
}
}
else // Otherwise print the most recent keypress
{
seat.print(lastKey);
inputCount ++; // increment the count of input events
}
}
}