Monday 30 September 2024

Web Tutorial: ReactJS Liar's Dice (Part 4/4)

We've gone through a bit of gamplay. Now it's time to handle winning and losing.

It doesn't take a single win or loss to decide a stage. The stage is only won or lost when either player or oppoenent's intoxication level is 0, and said level is reduced by losing rounds.

So first, let's add winning and losing to the phrases. "win" and "lose" are for individual rounds. "stagewin" and "stagelose" are for entire stages.

src/utils/GetPhrases.js
{ personality: 1, phraseName: "openup", lang: "en", value: "Can you show me your dice?"},
{ personality: 1, phraseName: "openup", lang: "cn", value: "能开给我看吗?"},
{ personality: 1, phraseName: "openup", lang: "en", value: "I think we should open up!"},
{ personality: 1, phraseName: "openup", lang: "cn", value: "我想我们开吧!"},
{ personality: 1, phraseName: "win", lang: "en", value: "Looks like I've won! Please have a drink."},
{ personality: 1, phraseName: "win", lang: "cn", value: "看来我赢了! 请喝酒!"},
{ personality: 1, phraseName: "win", lang: "en", value: "I was lucky!"},
{ personality: 1, phraseName: "win", lang: "cn", value: "侥幸而已!"},
{ personality: 1, phraseName: "lose", lang: "en", value: "Does this mean I lose?"},
{ personality: 1, phraseName: "lose", lang: "cn", value: "我这算是输了吗?"},
{ personality: 1, phraseName: "lose", lang: "en", value: "Please be gentle! I'll drink."},
{ personality: 1, phraseName: "lose", lang: "cn", value: "温柔点! 我喝."},
{ personality: 1, phraseName: "stagewin", lang: "en", value: "I can't believe I won!"},
{ personality: 1, phraseName: "stagewin", lang: "cn", value: "我赢了! 难以相信!"},
{ personality: 1, phraseName: "stagewin", lang: "en", value: "Thank you for going easy on me!"},
{ personality: 1, phraseName: "stagewin", lang: "cn", value: "多谢手下留情!"},
{ personality: 1, phraseName: "stagelose", lang: "en", value: "Oh, I think I drank too much..."},
{ personality: 1, phraseName: "stagelose", lang: "cn", value: "哦我喝多了..."},
{ personality: 1, phraseName: "stagelose", lang: "en", value: "You're too good at this! I can't beat you."},
{ personality: 1, phraseName: "stagelose", lang: "cn", value: "你太厉害了! 我赢不了你."}



You'l notice that right now, the round can go on forever regardless of who wins or loses, because we have not yet handed win conditions with the checkWin() function. So let's do that. We begin by declaring variable diceQty as 0.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
};


Then we use a For loop to go through both the opponentDice and playerDice arrays. For every dice value that is 1 or matches currentGuessDice, we increment diceQty.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
  for (var i = 0; i < 5; i++) {
    if (opponentDice[i] === 1 || opponentDice[i] === currentGuessDice) diceQty++;
    if (playerDice[i] === 1 || playerDice[i] === currentGuessDice) diceQty++;
  }

};


Declare correctGuess - it's true or false depending on whether diceQty is greater or equal to currentGuessQty.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
  for (var i = 0; i < 5; i++) {
    if (opponentDice[i] === 1 || opponentDice[i] === currentGuessDice) diceQty++;
    if (playerDice[i] === 1 || playerDice[i] === currentGuessDice) diceQty++;
  }

  var correctGuess = (diceQty >= currentGuessQty);
};


We then declare playerWin and assume it is true. It will be false if the opening up was called by the player (thus, isplayerOpen being true) and it's a correct guess, or if it was the opponent who called for opening up and the guess turned out to be wrong.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
  for (var i = 0; i < 5; i++) {
    if (opponentDice[i] === 1 || opponentDice[i] === currentGuessDice) diceQty++;
    if (playerDice[i] === 1 || playerDice[i] === currentGuessDice) diceQty++;
  }

  var correctGuess = (diceQty >= currentGuessQty);

  var playerWin = true;
  if (isPlayerOpen && correctGuess) playerWin = false;
  if (!isPlayerOpen && !correctGuess) playerWin = false;

};


We then handle the cases for if the payer won or lost. If the player won, we set opponentDialog using the phrase we created earlier, and reduce the opponentIntoxication value (because the opponent takes a drink). The minimum is 0, so we need to check for that first.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
  for (var i = 0; i < 5; i++) {
    if (opponentDice[i] === 1 || opponentDice[i] === currentGuessDice) diceQty++;
    if (playerDice[i] === 1 || playerDice[i] === currentGuessDice) diceQty++;
  }

  var correctGuess = (diceQty >= currentGuessQty);

  var playerWin = true;
  if (isPlayerOpen && correctGuess) playerWin = false;
  if (!isPlayerOpen && !correctGuess) playerWin = false;

  if (playerWin) {
    setOpponentDialog(GetPhrases(stage, "lose", lang));
    var intoxication = opponentIntoxication - (35 - (stage * 5));
    if (intoxication < 0) intoxication = 0;
    setOpponentIntoxication(intoxication);

    if (intoxication === 0) setOpponentDialog(GetPhrases(stage, "stagelose", lang));
  } else {

  }

};


And if the payer loses, we do the same, but with differences in opponentDialog. And this time, it's payerIntoxication that gets reduced.

src/components/Game/Game.js
const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {
  var diceQty = 0;
  for (var i = 0; i < 5; i++) {
    if (opponentDice[i] === 1 || opponentDice[i] === currentGuessDice) diceQty++;
    if (playerDice[i] === 1 || playerDice[i] === currentGuessDice) diceQty++;
  }

  var correctGuess = (diceQty >= currentGuessQty);

  var playerWin = true;
  if (isPlayerOpen && correctGuess) playerWin = false;
  if (!isPlayerOpen && !correctGuess) playerWin = false;

  if (playerWin) {
    setOpponentDialog(GetPhrases(stage, "lose", lang));
    var intoxication = opponentIntoxication - (35 - (stage * 5));
    if (intoxication < 0) intoxication = 0;
    setOpponentIntoxication(intoxication);

    if (intoxication === 0) setOpponentDialog(GetPhrases(stage, "stagelose", lang));
  } else {
    setOpponentDialog(GetPhrases(stage, "win", lang));
    var intoxication = playerIntoxication - 35;
    if (intoxication < 0) intoxication = 0;
    setPlayerIntoxication(intoxication);

    if (intoxication === 0) setOpponentDialog(GetPhrases(stage, "stagewin", lang));

  }
};


Try again! We start by guessing four fives.


Opponent opens up, and it looks like the player loses. See? The intoxication bar goes down and changes color. Click End Round and Start New Round.


Guess four fours.


Opponent raises it to six fours.


Click Open Up, and it looks like the opponent loses. See her meter bar!


And loses another round.


And another. See the stripping going on?


Finally, one more loss and the meter reaches 0.


Before we continue, you really should add in the phrases for all five stages in the GetPhrases utility. The code is here.

Now, if you click End Round, you should get this (because stage gets incremented at this point). Rinse and repeat, till you clear all five stages! If, at any time he player's intoxication meter reaches 0, we go right back to that stage's Intro screen.


Final screen!

If you clear all five stages, the value of stage becomes 6. We'll handle it this way, inside the If block we created early in this web tutorial. If stage is 6, we will return a div with the id of Champion.

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">

    </div>        
  );

}


We have a h1 tag with a label we derive from calling GetLabels(). And a button styled using btnFinalQuit and actionButton, that runs the quit() function.

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">
      <h1>{ GetLabels("final", lang) }</h1>
      <p>
        <button className="btnFinalQuit actionButton"  onClick={ ()=>{ quit(); } }>{ GetLabels("quit", lang) } &#9650;</button>
      </p>
    </div>        
  );
}


The label is as so...

src/utils/GetLabels.js
{ labelName: "8dice", lang: "en", value: "Eight"},
{ labelName: "8dice", lang: "cn", value: "八个"},
{ labelName: "9dice", lang: "en", value: "Nine"},
{ labelName: "9dice", lang: "cn", value: "九个"},
{ labelName: "10dice", lang: "en", value: "Ten"},
{ labelName: "10dice", lang: "cn", value: "十个"},  
{ labelName: "final", lang: "en", value: "YOU ARE THE CHAMPION!"},
{ labelName: "final", lang: "cn", value: "你是冠军!"}



We want Champion to take up full width with a limited height.

src/components/Game/Game.css
.btnStartStage {
  margin: 50px auto 0 auto;
}

.btnQuit {
  margin: 20px auto 0 20px;
}

#Champion {
  width: 100%;
  height: 200px;
  float: left;
  text-align: center;
  font-size: 3em;
}


Like btnStartStage, btnFinalQuit is centered, so let it share the same styling.

src/components/Game/Game.css
.btnStartStage, .btnFinalQuit {
  margin: 50px auto 0 auto;
}


There's that button and message.


Time to showcase all the defeated opponents! Let's start with just one. We have a div styled using the final CSS class.

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">
      <h1>{ GetLabels("final", lang) }</h1>
      <p>
        <button className="btnFinalQuit actionButton"  onClick={ ()=>{ quit(); } }>{ GetLabels("quit", lang) } &#9650;</button>
      </p>

      <div className="final">

      </div>

    </div>        
  );
}


Then we add a div inside it. It will be styled using profile and  the CSS class string returned by calling GetOpponentImage(). In that function, we pass in 1 and 0 because we want the image for the first stage opponent but at 0 intoxication.

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">
      <h1>{ GetLabels("final", lang) }</h1>
      <p>
        <button className="btnFinalQuit actionButton"  onClick={ ()=>{ quit(); } }>{ GetLabels("quit", lang) } &#9650;</button>
      </p>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(1, 0) }></div>
      </div>
    </div>        
  );
}


Then we have another div styled using the words CSS class. In it, inside a span tag styled using opponentName, we gave the opponent's name and then after a break, we add in a phrase.

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">
      <h1>{ GetLabels("final", lang) }</h1>
      <p>
        <button className="btnFinalQuit actionButton"  onClick={ ()=>{ quit(); } }>{ GetLabels("quit", lang) } &#9650;</button>
      </p>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(1, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent1", lang) }</span><br />"{ GetPhrases(1, "stagelose", lang) }"</div>
      </div>
    </div>        
  );
}


The CSS is as follows. final will have only 20% width because there are going to be 5 of them. And it will be floated left. Any element styled using profile inside final, will have round corners and have a defined height and width.. The background is set to use one image without repeating. And finally, any element styled using words inside final, will have the properties as shown. Not a big deal, just more aesthetics. We don't need to specify the styling of opponentName; we already defined it in App.css earlier, so we're just reusing it now.

src/components/Game/Game.css
#Champion {
  width: 100%;
  height: 200px;
  float: left;
  text-align: center;
  font-size: 3em;
}

.final {
  width: 20%;
  float: left;
}

.final .profile {
  width: 150px;
  height: 150px;
  border-radius: 50%;
  background-size: cover;
  background-position: center top;
  background-repeat: no-repeat;
}

.final .words {
  padding: 1em;
  font-size: 0.5em;
}


See that?


Now let's fill in the rest!

src/components/Game/Game.js
if (stage === 6) {
  return (
    <div id="Champion">
      <h1>{ GetLabels("final", lang) }</h1>
      <p>
        <button className="btnFinalQuit actionButton"  onClick={ ()=>{ quit(); } }>{ GetLabels("quit", lang) } &#9650;</button>
      </p>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(1, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent1", lang) }</span><br />"{ GetPhrases(1, "stagelose", lang) }"</div>
      </div>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(2, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent2", lang) }</span><br />"{ GetPhrases(2, "stagelose", lang) }"</div>
      </div>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(3, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent3", lang) }</span><br />"{ GetPhrases(3, "stagelose", lang) }"</div>
      </div>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(4, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent4", lang) }</span><br />"{ GetPhrases(4, "stagelose", lang) }"</div>
      </div>

      <div className="final">
        <div className={ "profile " + GetOpponentImage(5, 0) }></div>
        <div className="words"><span className="opponentName">{ GetLabels("opponent5", lang) }</span><br />"{ GetPhrases(5, "stagelose", lang) }"</div>
      </div>

    </div>        
  );
}


Beautiful!


That's it! Time to write tests!

I'll be honest - this app wasn't all that well-written. I certainly didn't write it in a way that was easy to test. That's on me. Still, there are some basic ones we can write, and run.

For the main one, we an check for these elements.

src/App.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import App from "./App";

describe("App", () => {
  it("renders important elements", () => {
    render(<App />);
    expect(screen.getByTestId("start-button")).toBeInTheDocument();
    expect(screen.getByTestId("dashboard-language")).toBeInTheDocument();
    expect(screen.getByTestId("dashboard-dialogspeed")).toBeInTheDocument();
  });
});


Of course, since we're using the getByTestId() method, we'll need to add the appropriate attributes.

src/App.js
<button data-testid="start-button" onClick={ ()=>{ start(); }} className="actionButton" >{ GetLabels("start", lang) } &#9658;</button>


src/App.js
<label id="DashboardLanguage">
  { GetLabels("language", lang) } 
  <select data-testid="dashboard-language" onChange={ (e)=>{ setLang(e.currentTarget.value); }}>
    <option value="cn">CN</option>
    <option value="en">EN</option>
  </select>
</label>

<label id="DashboardDialogSpeed">
  { GetLabels("dialogSpeed", lang) } 
  <select data-testid="dashboard-dialogspeed" onChange={ (e)=>{ setDialogSpeed(e.currentTarget.value); }}>
    <option value="500">{ GetLabels("fast", lang) }</option>
    <option value="1000">{ GetLabels("medium", lang) }</option>
    <option value="1500">{ GetLabels("slow", lang) }</option>
  </select>
</label>


For the Game component, we'll need to import all this.

src/components/Game/Game.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Game from "./Game";

import GetLabels from "../../utils/GetLabels";


We declare some variables that will act as parameters for the elements we'll be testing. These are the values that would normally be passed down from App.

src/components/Game/Game.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Game from "./Game";

import GetLabels from "../../utils/GetLabels";

let lang = "en";
let dialogSpeed = 500;

let stage;
let setStage = (val)=> {
  stage = val;
};

let stageStarted;
let setStageStarted = (val)=> {
  stageStarted = val;
};



We first ensure that certain elements are not present when stage is 0.

src/components/Game/Game.test.js
let lang = "en";
let dialogSpeed = 500;

let stage;
let setStage = (val)=> {
  stage = val;
};

let stageStarted;
let setStageStarted = (val)=> {
  stageStarted = val;
};

describe("Game", () => {
  setStage(0);
  setStageStarted(false);

  it("renders nothing for stage 0", () => {
    render(
      <Game
        lang = { lang }
        dialogSpeed = { dialogSpeed }
        stage={ stage }
        setStage = { setStage }
        stageStarted={ stageStarted }
        setStageStarted = { setStageStarted }
      />
    );
    expect(screen.queryByTestId("game-main")).toBeNull();
  });
});



Again we have to set testing-id for the Main div. Not really something I would normally recommend.

src/components/Game/Game.js
<div id="Main" testing-id="game-main">


Then we will look for the text "YOU ARE THE CHAMPION!" when stage is 6 and the language is "en".

src/components/Game/Game.test.js
describe("Game", () => {
  setStage(0);
  setStageStarted(false);

  it("renders nothing for stage 0", () => {
    render(
      <Game
        lang = { lang }
        dialogSpeed = { dialogSpeed }
        stage={ stage }
        setStage = { setStage }
        stageStarted={ stageStarted }
        setStageStarted = { setStageStarted }
      />
    );
    expect(screen.queryByTestId("game-main")).toBeNull();
  });
});

describe("Game", () => {
  setStage(6);
  setStageStarted(true);

  it("renders champion div for stage 6", () => {
    render(
      <Game
        lang = { lang }
        dialogSpeed = { dialogSpeed }
        stage={ stage }
        setStage = { setStage }
        stageStarted={ stageStarted }
        setStageStarted = { setStageStarted }
      />
    );

    expect(screen.queryByText("YOU ARE THE CHAMPION!")).toBeInTheDocument();
  });
});



Finally, the Dice component, arguably the easiest one to test. These are all you'll need to import.

src/components/Dice/Dice.test.js

import React from "react";
import { render, screen } from "@testing-library/react";
import Dice from "./Dice";


If val is 6, the number of elements in the array returned by queryAllByTitle() with  "dot val1" in the argument, will naturally be 6. We programmed it to be in the title ourselves!

src/components/Dice/Dice.test.js
import React from "react";
import { render, screen } from "@testing-library/react";
import Dice from "./Dice";

describe("Dice", () => {
  it("renders dice for 6", () => {
    render(
      <Dice
        dice = "6"
        diceIndex = "0"
        classPrefix = "opponentDice"
        highlight = { false }
        show = { true }
      />
    );

    expect(screen.queryAllByTitle("dot val1").length).toBe(6);
  });
});


Similarly for val1!

src/components/Dice/Dice.test.js
describe("Dice", () => {
  it("renders dice for 6", () => {
    render(
      <Dice
        dice = "6"
        diceIndex = "0"
        classPrefix = "opponentDice"
        highlight = { false }
        show = { true }
      />
    );

    expect(screen.queryAllByTitle("dot val1").length).toBe(6);
  });
});

describe("Dice", () => {
  it("renders dice for 1", () => {
    render(
      <Dice
        dice = "1"
        diceIndex = "0"
        classPrefix = "opponentDice"
        highlight = { false }
        show = { true }
      />
    );

    expect(screen.queryAllByTitle("dot val1").length).toBe(1);
  });
});



Now we test for if show is false. When that's the case, no matter the value of val, the result is always 0.

src/components/Dice/Dice.test.js
describe("Dice", () => {
  it("renders dice for 1", () => {
    render(
      <Dice
        dice = "1"
        diceIndex = "0"
        classPrefix = "opponentDice"
        highlight = { false }
        show = { true }
      />
    );

    expect(screen.queryAllByTitle("dot val1").length).toBe(1);
  });
});

describe("Dice", () => {
  it("renders dice for 1 but not revealed", () => {
    render(
      <Dice
        dice = "1"
        diceIndex = "0"
        classPrefix = "opponentDice"
        highlight = { false }
        show = { false }
      />
    );

    expect(screen.queryAllByTitle("dot val1").length).toBe(0);
  });
});


We enter "npm test" in the CLI, and there it is!


Thanks for sticking with me!

Wasn't this fun? Well, it certainly was my idea of fun. Probably explains why I've been doing this for so many years.

Dice, dice, baby!
T___T

Friday 27 September 2024

Web Tutorial: ReactJS Liar's Dice (Part 3/4)

It's time to work on gameplay!

When we frst run startStage(), this sets stageStarted to true, and affects the visibility of certain elements. One of these is the button to start a new stage. Instead of just styling it using actionButton, make it be styled using hidden if roundStarted is true, or stageStarted is false, or if playerIntoxication is 0. (The last conditions means that the player has lost, and no more progression is possible). And otherwise, use actionButton.

src/components/Game/Game.js
<button className={ (roundStarted || !stageStarted || playerIntoxication === 0 ? "hidden" : "actionButton") }>{ GetLabels("startnewround", lang) } &#9658;</button>


Now, we want this button to run startNewRound() when clicked.

src/components/Game/Game.js
<button onClick={ ()=>{ startNewRound(); } } className={ (roundStarted || !stageStarted || playerIntoxication === 0 ? "hidden" : "actionButton") }>{ GetLabels("startnewround", lang) } &#9658;</button>


Le's create the function.

src/components/Game/Game.js
const startStage = function() {
  setPlayerIntoxication(100);
  setOpponentIntoxication(100);
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setOpponentDice([1, 1, 1, 1, 1]);
  setPlayerDice([1, 1, 1, 1, 1]);
  setRound(1);
  setTurns(0);
  setShow(false);
  setStageStarted(true);

  setOpponentDialog(GetPhrases(stage, "newround", lang));
};

const startNewRound = function() {
  
};


const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};


We set show to true. show is a flag that determines if the opponent's dice are revealed. We also set shake to true. shake is a flag that tells the system that the dice are being "shaken". And thirdly, we set roundStarted to true.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);  
};


Since shake has been set to true, that means the dice are "shaking". Declare variable shaking, and set it to the value returned by running the setInterval() function. This will run every 10 milliseconds. Since the dice are being "shaken", we want it to appear as if the numbers are constantly changing.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    
  },
  100);

};


Declare values_opponent and values_player as empty arrays.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

  },
  100);
};


There are five dice for the opponent and the plyer, each. So we use For loop that will run five times. We populate values_opponent and values_player with a random number from 1 to 6. By the time we exit the For loop, these two arrays should have five values each.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

  },
  100);
};


Set opponentDice and playerDice to the values of values_opponent and values_player, respectively.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

    setOpponentDice(values_opponent);
    setPlayerDice(values_player);  

  },
  100);
};


Then run setTimeout(), setting the delay to 1 second.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

    setOpponentDice(values_opponent);
    setPlayerDice(values_player);  
  },
  100);

  setTimeout(
    ()=> {

    },
    1000
  );  

};


In it, we set show and shake to false, and use clearInterval() to stop the timer for shaking. This means that the dice shaking lasts only for 1 second. In addition, round is incremented, turns is set to 0 and isPlayerTurn is set to true. Which basically means every new round, the player goes first.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

    setOpponentDice(values_opponent);
    setPlayerDice(values_player);  
  },
  100);

  setTimeout(
    ()=> {
      setShow(false);
      setShake(false);
      clearInterval(shaking);
      setRound(round + 1);
      setTurns(0);
      setIsPlayerTurn(true);

    },
    1000
  );  
};


Since it's the player's turn, we run setOpponentDialog() to nudge the player.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

    setOpponentDice(values_opponent);
    setPlayerDice(values_player);  
  },
  100);

  setTimeout(
    ()=> {
      setShow(false);
      setShake(false);
      clearInterval(shaking);
      setRound(round + 1);
      setTurns(0);
      setIsPlayerTurn(true);

      setOpponentDialog(GetPhrases(stage, "yourturn", lang));
    },
    1000
  );  
};


Add "yourturn" phrases to the phrases array in the GetPhrases utility.

src/utils/GetPhrases.js
let phrases = [
  { personality: 1, phraseName: "intro", lang: "en", value: "Hello, I'm Little Grass. Please go easy on me!"},
  { personality: 1, phraseName: "intro", lang: "cn", value: "你好, 我是小草. 请高抬贵手!"},
  { personality: 1, phraseName: "intro", lang: "en", value: "Hello, I'm Little Grass. I'm new at this. Pease show me the ropes!"},
  { personality: 1, phraseName: "intro", lang: "cn", value: "你好, 我是小草. 我对这游戏不是很熟. 请多指教!"},
  { personality: 1, phraseName: "newround", lang: "en", value: "I'm so excited. Let's start!"},
  { personality: 1, phraseName: "newround", lang: "cn", value: "好兴奋! 开始吧!"},
  { personality: 1, phraseName: "newround", lang: "en", value: "Please go slow!"},
  { personality: 1, phraseName: "newround", lang: "cn", value: "慢点哦!"},
  { personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, right?"},
  { personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了,对吗?"},
  { personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, what will you do?"},
  { personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了, 怎么做?"}


When you click Start New Round, see the text in the speech balloon change. The dice change values rapidly for 1 second before settling, and the Start New Round button disappears!


But the dice shakers still haven't shaken. We need to adjust things this way. Instead of just styling using the shaker CSS class, add the class shaking if shake is true.

src/components/Game/Game.js
<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>

  <div className="right width_long">
      {
        opponentDice.map(function(dice, diceIndex){
          return (
          <Dice
              dice = { dice }
              diceIndex = { diceIndex }
              classPrefix = "opponentDice"
            />
          );
        })
      }
  </div>
</div>  

<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>

  <div className="right width_long">
      {
        playerDice.map(function(dice, diceIndex){
          return (
            <Dice
              dice = { dice }
              diceIndex = { diceIndex }
              classPrefix = "playerDice"
            />
          );
        })
      }
  </div>
</div>  


For shaking, we specify the animation to be shakingAnimation, and that the duration is half a second.

src/components/Game/Game.css
.shaker {
  width: 80px;
  height: 80px;
  margin: 0 auto 0 auto;
  background-image: url(../../img/shaker.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;
}

.shaking {
  animation-name: shakingAnimation;
  animation-duration: 0.5s;
}


.guessQty {
  font-weight: bold;
  font-size: 2.5em;
  text-align: right;
}


shakingAnimation specifies that the element moves up and down. It'll look like it's jiggling.

src/components/Game/Game.css
.shaker {
  width: 80px;
  height: 80px;
  margin: 0 auto 0 auto;
  background-image: url(../../img/shaker.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;
}

.shaking {
  animation-name: shakingAnimation;
  animation-duration: 0.5s;
}

@keyframes shakingAnimation {
  0%   { margin: 5px auto 0 auto; }
  25%  { margin: 0 auto 0 auto; }
  50%  { margin: 5px auto 0 auto; }
  75%  { margin: 0 auto 0 auto; }
  100% { margin: 5px auto 0 auto; }
}


.guessQty {
  font-weight: bold;
  font-size: 2.5em;
  text-align: right;
}


Let's write some code to handle the effects of the flag show on Dice. The value of show will be passed into props.

src/components/Dice/Dice.js
function Dice(props) {
  let dice = props.dice;
  let diceIndex = props.diceIndex;
  let classPrefix = props.classPrefix;
  let show = props.show;

  var dots = [


Further down in the function, where the string css is defined, we further define it with a conditional block. Our previous definition stands only if show is true, otherwise the string is "dot hideDice".

src/components/Dice/Dice.js
dots[dice - 1].map(function(dot, dotIndex){
  var css = (show ? "dot val" + dot : "dot hideDice");

  return <div className={ css } title={ css } key={ classPrefix + diceIndex + "_" + dotIndex }>

  </div>
})


This is the CSS for hideDice. Firstly, hideDice will be styled like val1 as far as the dot is concerned. But the color will be a deep grey.

src/components/Dice/Dice.css
.hideDice::after, .val1::after {
  display: block;
  content: "";
  margin: 1px 0 0 1px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

.opponentDice .val1::after, .guessDice .val1::after {
  border: 2px solid rgb(200, 200, 200);
  background-color: rgb(180, 180, 180);
}

.playerDice .val1::after {
  border: 2px solid rgb(180, 0, 0);
  background-color: rgb(100, 0, 0);
}

.hideDice::after {
  border: 2px solid rgb(50, 50, 50);
  background-color: rgb(30, 30, 30);
}


And here, make sure we add show into the component. Note that for the player's dice, show is always true.

src/components/Game/Game.js
<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>

  <div className="right width_long">
      {
        opponentDice.map(function(dice, diceIndex){
          return (
            <Dice
              dice = { dice }
              diceIndex = { diceIndex }
              classPrefix = "opponentDice"
              show = { show }
            />
          );
        })
      }
  </div>  
</div>  

<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>

  <div className="right width_long">
      {
        playerDice.map(function(dice, diceIndex){
          return (
            <Dice
              dice = { dice }
              diceIndex = { diceIndex }
              classPrefix = "playerDice"
              show = { true }
            />
          );
        })
      }
  </div>
</div>  


And here. show is always true for this context, as well.

src/components/Game/Game.js
<div className="left width_long">
    <Dice
      dice = { playerGuessDice }
      diceIndex = "0"
      classPrefix = "guessDice"
      show = { true }
    />
</div>


As you can see, now the opponent's dice are obscured!


Let's handle some of the other buttons. This button runs the restartStage() function. It appears only when playerIntoxication is 0, which means the player has lost the game. Otherwise, it's hidden.

src/components/Game/Game.js
<button className="actionButton">{ GetLabels("endround", lang) } &#9673;</button>
<button onClick={ ()=>{ startNewRound(); } } className={ (roundStarted || !stageStarted || playerIntoxication === 0 ? "hidden" : "actionButton") }>{ GetLabels("startnewround", lang) } &#9658;</button>
<button onClick={ ()=>{ restartStage(); } } className={ (playerIntoxication === 0 ? "actionButton" : "hidden") }>{ GetLabels("restartstage", lang) } &#9658;</button>


Here's the restartStage() function. We set these values back to their defaults. Note that stageStarted and roundStarted are false, which should send the user back to that stage's intro screen.

src/components/Game/Game.js
function quit() {
  setStage(0);
  setRoundStarted(false);
  setStageStarted(false);
  setGameStarted(false);
}

const restartStage = function() {
  setPlayerIntoxication(100);
  setOpponentIntoxication(100);
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setIsPlayerTurn(true);
  setStageStarted(false);
  setRoundStarted(false);
};


const startStage = function() {
  setPlayerIntoxication(100);
  setOpponentIntoxication(100);
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setOpponentDice([1, 1, 1, 1, 1]);
  setPlayerDice([1, 1, 1, 1, 1]);
  setRound(1);
  setTurns(0);
  setShow(false);
  setStageStarted(true);

  setOpponentDialog(GetPhrases(stage, "newround", lang));
};


This button runs endRound(). It only appears when roundStarted and show are true, and playerIntoxication is greater than 0. This means that the round has started, and ended (because the dice are now shown). If playerIntoxication is still greater than 0, the game can continue, thus the user gets to end the round.

src/components/Game/Game.js
<button onClick={ ()=>{ endRound();} } className={ (roundStarted && show && playerIntoxication > 0 ? "actionButton" : "hidden") }>{ GetLabels("endround", lang) } &#9673;</button>
<button onClick={ ()=>{ startNewRound(); } } className={ (roundStarted || !stageStarted || playerIntoxication === 0 ? "hidden" : "actionButton") }>{ GetLabels("startnewround", lang) } &#9658;</button>
<button onClick={ ()=>{ restartStage(); } } className={ (playerIntoxication === 0 ? "actionButton" : "hidden") }>{ GetLabels("restartstage", lang) } &#9658;</button>


When ending the round, we reset these values back to their defaults. Then we check if opponentIntoxication is still greater than 0.

src/components/Game/Game.js
const startNewRound = function() {
  setShow(true);
  setShake(true);
  setRoundStarted(true);
  
  var shaking = setInterval(()=>{
    var values_opponent = [];
    var values_player = [];

    for (var i = 0; i < 5; i++) {
      var val = Math.floor(Math.random() * 6) + 1;
      values_opponent.push(val);
      val = Math.floor(Math.random() * 6) + 1;
      values_player.push(val);
    }

    setOpponentDice(values_opponent);
    setPlayerDice(values_player);  
  },
  100);

  setTimeout(
    ()=> {
      setShow(false);
      setShake(false);
      clearInterval(shaking);
      setRound(round + 1);
      setTurns(0);
      setIsPlayerTurn(true);

      setOpponentDialog(GetPhrases(stage, "yourturn", lang));
    },
    1000
  );  
};

const endRound = function() {
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setRoundStarted(false);
  setShow(false);

  if (opponentIntoxication > 0) {

  } else {

  }
}


const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};


If so, we indicate a new round by setting opponentDialog. But if not, we move on to the next opponent by incrementing stage. We act as we would when starting a new stage, by setting stageStarted to false, round to 0, turns to 0, resetting setPlayerIntoxication and setOpponentIntoxication, and setting opponentDialog to intro text.

src/components/Game/Game.js
const endRound = function() {
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setRoundStarted(false);
  setShow(false);

  if (opponentIntoxication > 0) {
    setOpponentDialog(GetPhrases(stage, "newround", lang));
  } else {
    setStage(stage + 1);
    setStageStarted(false);
    setRound(0);
    setTurns(0);
    setPlayerIntoxication(100);
    setOpponentIntoxication(100);
    setOpponentDialog(GetPhrases(stage, "intro", lang));
  }

}


Now you should only see the Start New Round button.


Let's work on the Guess and Open buttons. They'll run guess() and openup() functions respectively.

src/components/Game/Game.js
<div className="right width_short">
  <button onClick={ ()=>{ guess(); } } className="actionButton">{ GetLabels("guess", lang) }</button>
  <button onClick={ ()=>{ openup(); } } className="actionButton">{ GetLabels("openup", lang) }</button>
</div>


Here are the functions...

src/components/Game/Game.js
const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};

const guess = function() {

};

const openup = function() {

};


if (stage >= 1 && stage <= 5) {


For guess(), we set opponentDialog for some flavor text (to be added later), then increment turns. Then we implement a delay of dialogSpeed milliseconds.

src/components/Game/Game.js
const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};

const guess = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));
  setTurns(turns + 1);

  window.setTimeout(()=> {

  },
  dialogSpeed);

};

const openup = function() {

};

if (stage >= 1 && stage <= 5) {


After the delay, we set guessQty to playerGuessQty and guessDice to playerGuessDice. Why? Explain later. Since guessing counts as a turn, it's no longer the player's turn and thus we set isPlayerTurn to false. After that, we run the opponentAction() function because it's the opponent's turn, and pass in the values of playerGuessQty and playerGuessDice.

src/components/Game/Game.js
const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};

const guess = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));
  setTurns(turns + 1);

  window.setTimeout(()=> {
    setGuessQty(playerGuessQty);
    setGuessDice(playerGuessDice);
    setIsPlayerTurn(false);
    opponentAction(playerGuessQty, playerGuessDice);

  },
  dialogSpeed);
};

const openup = function() {

};

if (stage >= 1 && stage <= 5) {


Here is the opponentAction() function. Leave it blank for now. Note that there are two parameters.

src/components/Game/Game.js
const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};

const opponentAction = function(currentGuessQty, currentGuessDice) {

};


const guess = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));
  setTurns(turns + 1);

  window.setTimeout(()=> {
    setGuessQty(playerGuessQty);
    setGuessDice(playerGuessDice);
    setIsPlayerTurn(false);
    opponentAction(playerGuessQty, playerGuessDice);
  },
  dialogSpeed);
};


For openup(), we again set opponentDialog for some flavor, then implement a delay of dialogSpeed milliseconds. When we want to open up, it means to show the die and call your opponent's bluff. Therefore, show is set to true. After opening up, we check if the player has won or lost, so run the checkWin() function. We pass in true as an argument to indicate that it's the player who requested to open up (you'll see why very soon), then the values of guessQty and guessDice.

src/components/Game/Game.js
const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};

const guess = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));
  setTurns(turns + 1);

  window.setTimeout(()=> {
    setGuessQty(playerGuessQty);
    setGuessDice(playerGuessDice);
    setIsPlayerTurn(false);
    opponentAction(playerGuessQty, playerGuessDice);
  },
  dialogSpeed);
};

const openup = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));

  window.setTimeout(()=> {
    setShow(true);
    checkWin(true, guessQty, guessDice);
  },
  dialogSpeed);

};

if (stage >= 1 && stage <= 5) {


Declare checkWin(), but leave it empty for now. However, do add in the parameters isPlayerOpen, currentGuessQty and currentGuessDice.

src/components/Game/Game.js
const openup = function() {
  setOpponentDialog(GetPhrases(stage, "doubt", lang));

  window.setTimeout(()=> {
    setShow(true);
    checkWin(true, guessQty, guessDice);
  },
  dialogSpeed);
};

const checkWin = function(isPlayerOpen, currentGuessQty, currentGuessDice) {

};


if (stage >= 1 && stage <= 5) {


Now, before we do anything else, playerDashboard and guessDashboard need to be hidden under the right conditions. playerDashboard will be hidden (very briefly) when it's not the player's turn and the round has not started. guessDashboard is shown only when show and shake are false, and (obviously) it is the player's turn.

src/components/Game/Game.js
<div id="playerDashboard" className={ (isPlayerTurn && roundStarted ? "" : "hidden") }>
  <div id="guessDashboard" className={ (!show && !shake && isPlayerTurn ? "" : "hidden") }>


Before we forget, add in the phrases for "doubt" in the GetPhrases utility.
let phrases = [
  { personality: 1, phraseName: "intro", lang: "en", value: "Hello, I'm Little Grass. Please go easy on me!"},
  { personality: 1, phraseName: "intro", lang: "cn", value: "你好, 我是小草. 请高抬贵手!"},
  { personality: 1, phraseName: "intro", lang: "en", value: "Hello, I'm Little Grass. I'm new at this. Pease show me the ropes!"},
  { personality: 1, phraseName: "intro", lang: "cn", value: "你好, 我是小草. 我对这游戏不是很熟. 请多指教!"},
  { personality: 1, phraseName: "newround", lang: "en", value: "I'm so excited. Let's start!"},
  { personality: 1, phraseName: "newround", lang: "cn", value: "好兴奋! 开始吧!"},
  { personality: 1, phraseName: "newround", lang: "en", value: "Please go slow!"},
  { personality: 1, phraseName: "newround", lang: "cn", value: "慢点哦!"},
  { personality: 1, phraseName: "doubt", lang: "en", value: "Are we supposed to play like that?"},
  { personality: 1, phraseName: "doubt", lang: "cn", value: "是这样玩的吗?"},
  { personality: 1, phraseName: "doubt", lang: "en", value: "Let me think..."},
  { personality: 1, phraseName: "doubt", lang: "cn", value: "我想想哦..."},

  { personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, right?"},
  { personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了,对吗?"},
  { personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, what will you do?"},
  { personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了, 怎么做?"}


Rerun the code, and click through till you start Stage 1. There should be only the Start New Round button visible. Click it...


...and the button disappears! Now the guess controls are visible!


Great! We will now work on the controls. They will be disabled under certain conditions.

src/components/Game/Game.js
<div className="right width_short">
  <button onClick={ ()=>{ guess(); } } disabled={  } >{ GetLabels("guess", lang) }</button>
  <button onClick={ ()=>{ openup(); } } disabled={  } >{ GetLabels("openup", lang) }</button>
</div>


The Guess button will be disabled if running the isValidGuess() function with playerGuessQty and playerGuessDice gets you false, which means the current values of playerGuessQty and playerGuessDice are invalid for guessing. We will work on that function soon.

src/components/Game/Game.js
<div className="right width_short">
  <button onClick={ ()=>{ guess(); } } disabled={ (isValidGuess(playerGuessQty, playerGuessDice) ? "" : "disabled") } className="actionButton">{ GetLabels("guess", lang) }</button>
  <button onClick={ ()=>{ openup(); } } disabled={  } >{ GetLabels("openup", lang) }</button></div>


The Open Up button is disabled if show is already true, or if guessQty and guessDice are at their minimum values.

src/components/Game/Game.js
<div className="right width_short">
  <button onClick={ ()=>{ guess(); } } disabled={ (isValidGuess(playerGuessQty, playerGuessDice) ? "" : "disabled") } className="actionButton">{ GetLabels("guess", lang) }</button>
  <button onClick={ ()=>{ openup(); } } disabled={ ((guessQty === 3 && guessDice === 2) || show ? "disabled" : "") } className="actionButton">{ GetLabels("openup", lang) }</button>
</div>


Now here's the function. It's pretty simple. qty and guessDice are parameters. As long as qty is greater than the current guess quantity or dice is greater than the current dice guess value, it's valid and we return true. Of course, qty also has to be greater than 3.

src/components/Game/Game.js
const endRound = function() {
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setRoundStarted(false);
  setShow(false);

  if (opponentIntoxication > 0) {
    setOpponentDialog(GetPhrases(stage, "newround", lang));
  } else {
    setStage(stage + 1);
    setStageStarted(false);
    setRound(0);
    setTurns(0);
    setPlayerIntoxication(100);
    setOpponentIntoxication(100);
    setOpponentDialog(GetPhrases(stage, "intro", lang));
  }
}

const isValidGuess = function(qty, dice) {
  return ((qty > guessQty || dice > guessDice) && qty > 3);
};


const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};


See? The value of guessQty is 3 and the value of guessDice is 2. Both buttons are disabled!


Now let's deal with the Up and Down buttons for the guess quantity. They will call the adjustPlayerGuessQty() function, but with different arguments.

src/components/Game/Game.js
<div className="left width_half">
  <div className="guessQty left width_long">
    { playerGuessQty }
  </div>
  <div className="guessButtons right width_short">
    <button onClick={ ()=>{ adjustPlayerGuessQty(1); } }>&#9650;</button>
    <br />
    <button onClick={ ()=>{ adjustPlayerGuessQty(-1); } }>&#9660;</button>
  </div>
</div>


Here is the function. It has a parameter, inc. We first define finalQty as the sum of the current value of playerGuessQty, and inc. So finalQty will be the projected quantity after playerGuessQty is incremented or decremented.

src/components/Game/Game.js
const endRound = function() {
  setGuessQty(3);
  setGuessDice(2);
  setPlayerGuessQty(3);
  setPlayerGuessDice(2);
  setRoundStarted(false);
  setShow(false);

  if (opponentIntoxication > 0) {
    setOpponentDialog(GetPhrases(stage, "newround", lang));
  } else {
    setStage(stage + 1);
    setStageStarted(false);
    setRound(0);
    setTurns(0);
    setPlayerIntoxication(100);
    setOpponentIntoxication(100);
    setOpponentDialog(GetPhrases(stage, "intro", lang));
  }
}

const adjustPlayerGuessQty = function(inc) {
  var finalQty = playerGuessQty + inc;
};


const isValidGuess = function(qty, dice) {
  return ((qty > guessQty || dice > guessDice) && qty > 3);
};


The minimum value is guessQty and the maximum is 10 (because the total number of dice is 10). Thus, if the value of finalQty falls outside of these values, exit the function.

src/components/Game/Game.js
const adjustPlayerGuessQty = function(inc) {
  var finalQty = playerGuessQty + inc;
  if (finalQty < guessQty || finalQty > 10) return;
};


And at the end of the function, now that we've verified that finalQty is valid, we set playerGuessQty to finalQty.

src/components/Game/Game.js
const adjustPlayerGuessQty = function(inc) {
  var finalQty = playerGuessQty + inc;
  if (finalQty < guessQty || finalQty > 10) return;

  setPlayerGuessQty(finalQty);
};


There you go. When you click the Up and Down buttons, the quantity should change accordingly. You can't go above 10, and once you rise above 3, note that the Guess button is enabled!


Similar to the last set of Up and Down buttons, a function is triggered when these buttons are clicked. In this case, the function is adjustPlayerGuessDice(). As before, we either pass in 1 or -1 as arguments.

src/components/Game/Game.js
<div className="right width_half">
  <div className="left width_long">
  <Dice
    dice = { playerGuessDice }
    diceIndex = "0"
    classPrefix = "guessDice"
    show = { true }
  />
  </div>
  <div className="guessButtons right width_short">
    <button onClick={ ()=>{ adjustPlayerGuessDice(1); } }>&#9650;</button>
    <br />
    <button onClick={ ()=>{ adjustPlayerGuessDice(-1); } }>&#9660;</button>
  </div>
</div>


Similar principle to the last function we wrote. In this case, the final number can't be greater than 6.

src/components/Game/Game.js
const adjustPlayerGuessQty = function(inc) {
  var finalQty = playerGuessQty + inc;
  if (finalQty < guessQty || finalQty > 10) return;

  setPlayerGuessQty(finalQty);
};

const adjustPlayerGuessDice = function(inc) {
  var finalDice = playerGuessDice + inc;
  if (finalDice < guessDice || finalDice > 6) return;

  setPlayerGuessDice(finalDice);
};


const isValidGuess = function(qty, dice) {
  return ((qty > guessQty || dice > guessDice) && qty > 3);
};


Try it! The dice will change accordingly. You can't go above 6.


Remember if you click on the Guess button, it calls guess(). And guess() eventually calls opponentAction(). That's what we will work on next. We begin by declaring action. Then we derive its value by running the GetActions() function, passing in the arguments presented below.

src/components/Game/Game.js
const opponentAction = function(currentGuessQty, currentGuessDice) {
  var action = GetActions(stage, turns, currentGuessQty, currentGuessDice, opponentDice, opponentIntoxication);
};


GetActions is a utility that we are going to write. It basically determines your opponent's next move depending on certain variables. Create GetActions.js in the utils directory. The GetActions() function has these parameters to help determine the opponent's next action.

src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {

}

export default GetActions;  


First, we declare action as an object. The type property is "open". qty and dice only matter if type is "guess", but we'll just add these in because they might need to be changed later. By default, we return action. This means that by default, the opponent chooses to open up.

src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  return action;

}

export default GetActions;  


However, we have to check qty and dice, which represent the current guess in the game. If qty and dice are at the minimum (4 and 2 respectively) opening up is not a valid move. Thus type is set to "guess".

src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  if (qty === 4 && dice === 2) { 
    action.type = "guess";
  } else {

  }


  return action;
}

export default GetActions;  


Now we declare intelligence, which is the effective intelligence of the current opponent. The higher the value of stage, the more intelligent. Which means as the player progresses his opponents become "smarter". Also, intoxication plays a part, so intelligence is offset by intoxication. The more sober the opponent is, the "cleverer" she is. We then check if the value of intelligence is above a certain threshold, say, 50.

src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  if (qty === 4 && dice === 2) { 
    action.type = "guess";
  } else {
    var intelligence = (stage * 10) + intoxication;

    if (intelligence >= 50) {

    } else {

    }

  }

  return action;
}

export default GetActions;  


Now we try to calculate the most reasonable course of action. We run the filter() method on the array ownDice to see how many are 1s or match the guess, dice, in the opponent's own dice. The size of the resultant array is assigned to ownQty. If the guessed qty is relatively equal to ownQty, the opponent will guess some more. If the variance is too great, type is set to "open".
src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  if (qty === 4 && dice === 2) { //if minimal, always guess
    action.type = "guess";
  } else {
    var intelligence = (stage * 10) + intoxication;

    if (intelligence >= 50) {
      var ownQty = ownDice.filter((x) => { return x === 1 || x === dice; } ).length;

      action.type = (qty <= ownQty + 3 ? "guess" : "open");

    } else {

    }
  }

  return action;
}

export default GetActions;  


If intelligence fails, the opponent will randomly guess or open up. However, at the end of it, I put in one last clause to state that if qty is 8 or more, the opponent always chooses to open up.

src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  if (qty === 4 && dice === 2) { //if minimal, always guess
    action.type = "guess";
  } else {
    var intelligence = (stage * 10) + intoxication;

    if (intelligence >= 50) {
      var ownQty = ownDice.filter((x) => { return x === 1 || x === dice; } ).length;

      action.type = (qty <= ownQty + 3 ? "guess" : "open");
    } else {
      var rand = Math.floor(Math.random() * 2);
      action.type = (rand === 0 ? "guess" : "open");

    }

    if (qty >= 8) action.type = "open";
  }

  return action;
}

export default GetActions;  


Now, here's an If block to check if the action type is "guess". If so, we return action without waiting to get to the last line of the function.
src/utils/GetActions.js
const GetActions = (stage, turns, qty, dice, ownDice, intoxication) => {
  let action = { "type": "open", "qty": 0, "dice": 0};

  if (qty === 4 && dice === 2) { //if minimal, always guess
    action.type = "guess";
  } else {
    var intelligence = (stage * 10) + intoxication;

    if (intelligence >= 50) {
      var ownQty = ownDice.filter((x) => { return x === 1 || x === dice; } ).length;

      action.type = (qty <= ownQty + 3 ? "guess" : "open");
    } else {
      var rand = Math.floor(Math.random() * 2);
      action.type = (rand === 0 ? "guess" : "open");
    }

    if (qty >= 8) action.type = "open";
  }

  if (action.type === "guess") {
    return action;
  }


  return action;
}

export default GetActions;  


First, we define newQty. For its value, we will take qty and add a random small value to it. For newDice, we will add a random small value to dice. Note that newQty's value is always greater than qty's due to a "+1", whereas for newDice it's possible to remain exactly the same as dice.

src/utils/GetActions.js
if (action.type === "guess") {
  var newQty = Math.floor(Math.random() * 2 + 1) + qty;
  var newDice = Math.floor(Math.random() * 3) + dice;


  return action;
}


Now we set the qty and dice properties of action. However, even here there are limits. The qty property cannot be greater than 10. The dice property cannot be greater than 6.

src/utils/GetActions.js
if (action.type === "guess") {
  var newQty = Math.floor(Math.random() * 2 + 1) + qty;
  var newDice = Math.floor(Math.random() * 3) + dice;

  action.qty = (newQty > 10 ? 10 : newQty);
  action.dice = (newDice > 6 ? 6 : newDice);

  return action;
}


And, of course, import the uility.

src/components/Game/Game.js
import React, { useState } from 'react';
import './Game.css';
import Dice from '../../components/Dice';

import GetOpponentImage from '../../utils/GetOpponentImage';
import GetLabels from '../../utils/GetLabels';
import GetPhrases from '../../utils/GetPhrases';
import GetActions from '../../utils/GetActions';

function Game(props) {


Back to opponentAction()! After action is defined, we increment turns.
src/components/Game/Game.js
const opponentAction = function(currentGuessQty, currentGuessDice) {
  var action = GetActions(stage, turns, currentGuessQty, currentGuessDice, opponentDice, opponentIntoxication);
  setTurns(turns + 1);
};


Then we use If blocks, checking the value of action's type property.
src/components/Game/Game.js
const opponentAction = function(currentGuessQty, currentGuessDice) {
  var action = GetActions(stage, turns, currentGuessQty, currentGuessDice, opponentDice, opponentIntoxication);
  setTurns(turns + 1);

  if (action.type === "open") {

  }

  if (action.type === "guess") {

  }

};


Let's handle the opponent action of opening up. We first define dialogStr. It was be a combination of two strings returned fro calling GetPhrases(), joined with a newline character.

src/components/Game/Game.js
if (action.type === "open") {
  var dialogStr = (GetPhrases(stage, "myturn", lang) + "\n" + GetPhrases(stage, "openup", lang));
}


To display this as HTML, convert dialogStr to an array using the split() method and the newline character, then use the map() method on the array to return a series of paragraph tags with the content.
src/components/Game/Game.js
if (action.type === "open") {
  var dialogStr = (GetPhrases(stage, "myturn", lang) + "\n" + GetPhrases(stage, "openup", lang));
  var dialog = dialogStr.split('\n').map(i => {
    return <p>{i}</p>
  });

}


Then set opponentDialog to dialog.

src/components/Game/Game.js
if (action.type === "open") {
  var dialogStr = (GetPhrases(stage, "myturn", lang) + "\n" + GetPhrases(stage, "openup", lang));
  var dialog = dialogStr.split('\n').map(i => {
    return <p>{i}</p>
  });

  setOpponentDialog(dialog);
}


Since the opponent is opening up, set show to true.

src/components/Game/Game.js
if (action.type === "open") {
  var dialogStr = (GetPhrases(stage, "myturn", lang) + "\n" + GetPhrases(stage, "openup", lang));
  var dialog = dialogStr.split('\n').map(i => {
    return <p>{i}</p>
  });

  setOpponentDialog(dialog);
  setShow(true);
}


And then a number of milliseconds later (double dialogSpeed should be fine), run the checkWin() function.
src/components/Game/Game.js
if (action.type === "open") {
  var dialogStr = (GetPhrases(stage, "myturn", lang) + "\n" + GetPhrases(stage, "openup", lang));
  var dialog = dialogStr.split('\n').map(i => {
    return <p>{i}</p>
  });

  setOpponentDialog(dialog);
  setShow(true);
  window.setTimeout(()=> {
    checkWin(false, currentGuessQty, currentGuessDice);
  },
  dialogSpeed * 2);

}


Add the phrases in the GetPhrases utility for "myturn and "openup". While you're there, you may as well add in phrases for "guess".
src/utils/GetPhrases.js
{ personality: 1, phraseName: "doubt", lang: "en", value: "Are we supposed to play like that?"},
{ personality: 1, phraseName: "doubt", lang: "cn", value: "是这样玩的吗?"},
{ personality: 1, phraseName: "doubt", lang: "en", value: "Let me think..."},
{ personality: 1, phraseName: "doubt", lang: "cn", value: "我想想哦..."},
{ personality: 1, phraseName: "myturn", lang: "en", value: "I think it's my turn."},
{ personality: 1, phraseName: "myturn", lang: "cn", value: "好像轮到我了"},
{ personality: 1, phraseName: "myturn", lang: "en", value: "I should make a decision... "},
{ personality: 1, phraseName: "myturn", lang: "cn", value: "该我来了..."},  

{ personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, right?"},
{ personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了,对吗?"},
{ personality: 1, phraseName: "yourturn", lang: "en", value: "It's your turn, what will you do?"},
{ personality: 1, phraseName: "yourturn", lang: "cn", value: "到你了, 怎么做?"},
{ personality: 1, phraseName: "guess", lang: "en", value: "Let me see... I guess"},
{ personality: 1, phraseName: "guess", lang: "cn", value: "我想想... 我猜..."},
{ personality: 1, phraseName: "guess", lang: "en", value: "I'm guessing..."},
{ personality: 1, phraseName: "guess", lang: "cn", value: "我在想... 我猜..."},

{ personality: 1, phraseName: "openup", lang: "en", value: "Can you show me your dice?"},
{ personality: 1, phraseName: "openup", lang: "cn", value: "能开给我看吗?"},
{ personality: 1, phraseName: "openup", lang: "en", value: "I think we should open up!"},
{ personality: 1, phraseName: "openup", lang: "cn", value: "我想我们开吧!"}


Now for guessing. We start by using the action object, which contains qty and dice. We will set guessQty and playerGuessQty to qty. And we set guessDice and payerGuessDice to dice.
src/components/Game/Game.js
if (action.type === "guess") {
  setGuessQty(action.qty);
  setGuessDice(action.dice);
  setPlayerGuessQty(action.qty);
  setPlayerGuessDice(action.dice);

}


Then we define a delay (again, double dialogSpeed).
src/components/Game/Game.js
if (action.type === "guess") {
  setGuessQty(action.qty);
  setGuessDice(action.dice);
  setPlayerGuessQty(action.qty);
  setPlayerGuessDice(action.dice);

  window.setTimeout(()=> {

  },
  dialogSpeed * 2);

}


In it, we define dialogStr like we did the last time. This time, the string is longer and involves both the GetPhrases() and GetLabels() functions. This is because we not only want the opponent to guess, we want her to say how many quantities of values she is guessing. 

src/components/Game/Game.js
if (action.type === "guess") {
  setGuessQty(action.qty);
  setGuessDice(action.dice);
  setPlayerGuessQty(action.qty);
  setPlayerGuessDice(action.dice);

  window.setTimeout(()=> {
    var dialogStr = (GetPhrases(stage, "myturn", lang) + " " + GetPhrases(stage, "guess", lang) + " " + GetLabels(action.qty + "dice", lang) + GetLabels(action.dice + "s", lang) + "! \n" + GetPhrases(stage, "yourturn", lang));
  },
  dialogSpeed * 2);
}


Then we do what we did earlier for opening up, culminating in setting opponentDialog.
src/components/Game/Game.js
if (action.type === "guess") {
  setGuessQty(action.qty);
  setGuessDice(action.dice);
  setPlayerGuessQty(action.qty);
  setPlayerGuessDice(action.dice);

  window.setTimeout(()=> {
    var dialogStr = (GetPhrases(stage, "myturn", lang) + " " + GetPhrases(stage, "guess", lang) + " " + GetLabels(action.qty + "dice", lang) + GetLabels(action.dice + "s", lang) + "! \n" + GetPhrases(stage, "yourturn", lang));
    var dialog = dialogStr.split('\n').map(i => {
      return <p>{i}</p>
    });
    setOpponentDialog(dialog);

  },
  dialogSpeed * 2);
}


And here, because this concluds the opponent's turn, we set isPlayerTurn to true.
src/components/Game/Game.js
if (action.type === "guess") {
  setGuessQty(action.qty);
  setGuessDice(action.dice);
  setPlayerGuessQty(action.qty);
  setPlayerGuessDice(action.dice);

  window.setTimeout(()=> {
    var dialogStr = (GetPhrases(stage, "myturn", lang) + " " + GetPhrases(stage, "guess", lang) + " " + GetLabels(action.qty + "dice", lang) + GetLabels(action.dice + "s", lang) + "! \n" + GetPhrases(stage, "yourturn", lang));
    var dialog = dialogStr.split('\n').map(i => {
      return <p>{i}</p>
    });
    setOpponentDialog(dialog);
    setIsPlayerTurn(true);
  },
  dialogSpeed * 2);
}


Don't forget to add the labels here. 
src/utils/GetLabels.js
{ labelName: "guess", lang: "en", value: "Guess"},
{ labelName: "guess", lang: "cn", value: "猜"},  
{ labelName: "openup", lang: "en", value: "Open Up!"},
{ labelName: "openup", lang: "cn", value: "开!"},
{ labelName: "1s", lang: "en", value: " ones"},
{ labelName: "1s", lang: "cn", value: "一"},
{ labelName: "2s", lang: "en", value: " twos"},
{ labelName: "2s", lang: "cn", value: "二"},
{ labelName: "3s", lang: "en", value: " threes"},
{ labelName: "3s", lang: "cn", value: "三"},
{ labelName: "4s", lang: "en", value: " fours"},
{ labelName: "4s", lang: "cn", value: "四"},
{ labelName: "5s", lang: "en", value: " fives"},
{ labelName: "5s", lang: "cn", value: "五"},
{ labelName: "6s", lang: "en", value: " sixes"},
{ labelName: "6s", lang: "cn", value: "六"},
{ labelName: "1dice", lang: "en", value: "One"},
{ labelName: "1dice", lang: "cn", value: "一个"},
{ labelName: "2dice", lang: "en", value: "Two"},
{ labelName: "2dice", lang: "cn", value: "两个"},
{ labelName: "3dice", lang: "en", value: "Three"},
{ labelName: "3dice", lang: "cn", value: "三个"},
{ labelName: "4dice", lang: "en", value: "Four"},
{ labelName: "4dice", lang: "cn", value: "四个"},
{ labelName: "5dice", lang: "en", value: "Five"},
{ labelName: "5dice", lang: "cn", value: "五个"},
{ labelName: "6dice", lang: "en", value: "Six"},
{ labelName: "6dice", lang: "cn", value: "六个"},
{ labelName: "7dice", lang: "en", value: "Seven"},
{ labelName: "7dice", lang: "cn", value: "七个"},
{ labelName: "8dice", lang: "en", value: "Eight"},
{ labelName: "8dice", lang: "cn", value: "八个"},
{ labelName: "9dice", lang: "en", value: "Nine"},
{ labelName: "9dice", lang: "cn", value: "九个"},
{ labelName: "10dice", lang: "en", value: "Ten"},
{ labelName: "10dice", lang: "cn", value: "十个"}


Try this now. Start a game and make a guess. In this case, we guess four threes.


And see, your opponent tried to open up. She lost! But it's not really apparent. Something to fix soon.


Now try again. Restart the game. This time, we try guessing four twos.


Now the opponent raises the guess to six threes. See how the dice reflect her choice? Now, you can adjust the dice to raise another guess (notice how you can't go below 6 for quantity, or 3 for dice), or just open up.


And when you open up, it looks like your opponent lost! Again, it's not apparent. Let's fix this.


We want to define the isHighlightedDice() function. The parameter is dice. It returns true only if shake is false and show is true. Because highlighting dice only happens then.
src/components/Game/Game.js
const isValidGuess = function(qty, dice) {
  return ((qty > guessQty || dice > guessDice) && qty > 3);
};

const isHighlightedDice = function(dice) {
  return (!shake && show);
};


const getMeterColor = function(val) {
  if (val > 80) return "high";
  if (val > 50) return "half";
  return "low";
};


And then only if dice matches the value of guessDice or 1.
src/components/Game/Game.js
const isHighlightedDice = function(dice) {
  return (!shake && show && (dice === guessDice || dice === 1));
};


And then pass in the highlight attribute in every instance of the Dice component, using isHighlightedDice() as the value, with the value of dice passed in as an argument.
src/components/Game/Game.js
<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>  

  <div className="right width_long">
      {
    opponentDice.map(function(dice, diceIndex){
      return (
        <Dice
          dice = { dice }
          diceIndex = { diceIndex }
          classPrefix = "opponentDice"
          highlight = { isHighlightedDice(dice) }
          show = { show }
        />
      );
    })
  }        
  </div>  
</div>  

<div className="GameRow">
  <div className="left width_short">
    <div className={ "shaker " + (shake ? "shaking" : "") }></div>
  </div>  

  <div className="right width_long">
      {
    playerDice.map(function(dice, diceIndex){
      return (
        <Dice
          dice = { dice }
          diceIndex = { diceIndex }
          classPrefix = "playerDice"
          highlight = { isHighlightedDice(dice) }
          show = { true }
        />
      );
    })
  }
  </div>  
</div>  


In this instance, highlight is set to false because we never want to highlight this.
src/components/Game/Game.js
<div className="left width_long">
  <Dice
    dice = { playerGuessDice }
    diceIndex = "0"
    classPrefix = "guessDice"
    highlight = { false }
    show = { true }
  />
</div>


Now in the Dice component, we make sure to grab the value from props.
src/components/Dice/Dice.js
function Dice(props) {
  let dice = props.dice;
  let diceIndex = props.diceIndex;
  let classPrefix = props.classPrefix;
  let highlight = props.highlight;
  let show = props.show;


The div returned should be styled with highlighted_dice as well, or not, depending on the value of highlight.
src/components/Dice/Dice.js
return <div className={ "dice " + classPrefix + " " + (highlight ? "highlighted_dice" : "") } key={ classPrefix + diceIndex }>


And here's the CSS for that. Just gives the dice a 3 pixel red border.
src/components/Dice/Dice.css
.playerDice {
  background-color: rgb(255, 255, 250);
  border: 3px solid rgb(255, 255, 250);
}

.highlighted_dice {
  border: 3px solid rgb(255, 0, 0);
}


.dot {
  width: 16px;
  height: 16px;
  float: left;
}


And to see this clearer, let's disable the red outline in the main CSS.
src/App.css
div {
  outline: 0px solid red;
}


Let's try this. Start a game and get to a point where either you or the opponent opens up. See? The guess was six fives. When opening up, the dice that show 5 and 1, have a thick red border!


Phew! That was quite a chunk! But after this, it's mostly downhill.

Next

Win conditions and testing.