Friday 1 November 2024

Why people should (and shouldn't) hire older software developers

Seven years ago, I wrote this post on my 40th birthday. Today, as I turn 47 in a few days, here are some more thoughts.

On my website, I described myself as "an aging software developer". Some people have told me that this could reflect negatively on me. They're mistaken, but I forgive them - they aren't from the tech sector and don't know better. You see, when I say the word "aging", this is not me being humble. This is me flexing the fuck out.

Just showin' off.

But please, hear me out. I swear, I'm not going to pull out lazy clichés like "older programmers are more experienced, more mature, have more gravitas, etc" not just because they're lazy clichés, but also because they're not true. And if I have to explain why they're not true, perhaps this is not a conversation you're ready for.

And because I enjoy being contrary even against myself, I will follow that up by explaining why older devs aren't necessarily the best choice. In the same spirit, I will avoid the stereotypes of being inflexible, slow and outdated. Again, those are lazy clichés and we should rise above them.

Why older devs are nothing to sniff at

You see, software technology is an industry which demands constant reinvention. As a result, past a certain number of years in the business, older devs tend to go "fuck this constant re-learning. I'm gonna go drive a cab or something".

In short, it's an industry where you find few old folks. And you know what they say about being wary of old men in a trade where men die young. Well, programmers aren't dying per se, but they're certainly quitting once they hit a certain age, because, if one was just doing it for the money to begin with, at that point it's just not rewarding anymore.

And because software technology is an industry which demands constant reinvention, it almost goes without saying that anyone who's had to stick around for that long, has gone through quite a bit of that. Me personally, I went from desktop support to web portals, to commercial websites, to web and mobile applications, to having to shoulder the duties of an entire infocomm department all by my lonesome. Sure, one could say people who have survived that long in the industry are merely lucky, but very few people are that lucky.

Adaptability is key.

So, if you needed someone highly adaptable that could adapt to the constant change that defines this industry, who would you choose?

An enthusiastic youth with lots of potential to learn and grow and evolve and theoretically should be able to adapt? Or an older programmer who's actually evolved over and over through the years and survived to tell the tale?

The conventional wisdom, of course, is to go for the proven product rather than the one that has potential -in theory. Yes, some of us older folks can be rigid and stuck in our ways, but the nature of this industry weeds such people out fairly quickly. You're left with the people who are adaptable enough to survive this industry (because we have!), and in this day and age, that's no small thing.

Again, no argument would be complete without presenting the other side. And there are plenty of compelling reasons why the modern employer might not want an older developer.

Why you should avoid older devs

Older software developers, unless they totally mismanaged their wealth, tend to have money. The industry pays well, and even a mediocre dev like myself might be earning more than Middle Management at an SME. As such, you're not going to get one for cheap. We more than likely don't need the peanuts you're reserving for code monkeys.

Peanuts, anyone?

Manipulation. No matter how noble a person an employer thinks they are, this is an organization and as such, there's always something of that sort going on, to one degree or another. Against manipulation, many older devs have developed, if not outright immunity, at least a discomforting degree of resistance to it.

Past a certain age, most older devs already have whatever we ever wanted out of life. We're there. We're comfortable. We're not hungry and desperate as the younger ones probably are. We're not going to kill ourselves for "exposure". Or submit to opportunistic lowballing (c'mon, we all know it happens) just to add to our resume. And that is an absolute negative because cold as it may sound, it makes us less open to manipulation.

Older devs do come with experience, and part of that experience is security. We're generally zen about the fact that we'll probably die and be forgotten. We've come to terms with the realization that if we were really destined to do anything truly exceptional, statistically we would have done it decades ago. Most of us are too tired, or probably done too much, to feel that we need to prove a damn thing. And if you're the sort of employer who likes to make your staff jump through flaming hoops to prove themselves, again, that kind of security and self-assuredness makes us untenable as employees.

All in all, older developers aren't cheap, and we're not desperate or hungry enough to run through walls at your command. And I can't in good conscience paint that as a positive for employers.

In a nutshell

For the right kind of employer, older software developers are an exceptional resource.

Also, consider this - with the news that Artificial Intelligence is going to make programmers obsolete, whether it's true or not, this is going to impact the number of young developers available on the market. Because if younger programmers think that this career path is no longer viable, they're just going to leave, and who could blame them?

Us older programmers? We're dug in, and we have little to lose. Chew on that!

Old but gold,
T___T

Monday 14 October 2024

Teochew Thunder: Year Ten (Part 2/2)

The readership for this blog has never been astronomical. This year was no exception, though there were some bright spots in between.

Running that race.

To be fair, I have not been putting in all that much effort, having been very much distracted by offline activity. Still, I'm proud to say that it's a race I completed, if not exceled at, at least in comparison with previous years.

The Hits

Black Mirror film reviews did well predictably (as well as anything mentioning Artificial Intelligence) such as my review of Black Mirror Series Four. It never fails to bemuse me just how well readers seem to take to my Black Mirror reviews, especially since I don't think I'm that good a film reviewer.

Smoking' hot!

Artificial Intelligence seems to be a topic of great debate these days, and for good reason. Still, when I wrote How worried should software developers be about Devin AI?, I could not have predicted that people would take this much interest. After all, just about every software developer in the world had a take on it.

Singapore's Smoking Samsui Woman Controversy Through a Web Developer's Eyes was a particularly passionate rant of my own, and a pleasant surprise it gained as much traction as it did, since I only wrote about it a full month after it had become old news.

Decent Performers

This section is for the posts that garnered a fair amount of attention, at least where the average for this blog is concerned. It's for the ones that weren't hot topics - or perhaps they were, and I didn't handle the subject all that well. Still, they deserve something of a runner's up medal.

Not too bad.

Most web tutorials fall under this category this year. They weren't big hits, but a decent number of people viewed them, especially the multi-part ones. Does this mean I should make more of those?

Ode to my Lenovo, That strange feeling that comes with achieving that prize and A Software Developer's Vacation in Penang were personal accounts that dealt more with my life at large.

Five Phases of Programming I Went Through and Whose Fault Is It, Really? were more about my professional life, and some observations I had made over time.

D3 and HighCharts: Pros, Cons and Comparisons was a look at two similar JavaScript libraries. A bit on the dry, geeky side.

The Shits

This section is dedicated to the underperformers. Some of these had an OK amount of views but I expected so much more.

Not hot at all.

My ropework analogies rarely do that well, so I can't really say I'm surprised that The Slip Knot Analogy didn't get much views.

App Review: Duolingo might need more time. But as of now, its viewership is plodding along.

When an absence of value doesn't equate to a NULL was a disappointment. I really thought that a technical post, dry as it might be, would be a welcome entry in a tech blog.

So were Some use cases for JavaScript's Spread Syntax and Branching With Switch Statements for the same reasons outlined above.

The Tree of Newspaper and Toilet Roll Tubes: A TeochewThunder Project was me being vulnerable and all that shit. But oh well, it just didn't get that much traction.

Five Hilariously Unfortunate Names In Tech could have been so much more, but I suppose an international audience can't really appreciate the language-specific humor. Amusingly enough, the name Erlang Shen might have achieved mainstream status after the launch of the game Black Myth: Wukong.

Here's to Ten Years of Blogging!

It's been a decade of blogging. And I'm not done; not by a long shot.

Still trudging along.

There's more content coming up after this blogging break. More tech I'm exploring. More tech trends I'm following up on. More of everything.

And I'm loving every minute. It's a lot of work, but totally worth it.

Till Year Eleven,
T___T

Friday 11 October 2024

Teochew Thunder: Year Ten (Part 1/2)

Ten big ones. Ten!

This blog has been around for ten years since October in 2014. It feels like a momentous occasion, only it's really not, because I wasn't working up to this being any kind of milestone. In all honesty, I just kind of did my thing, rinse and repeat, and before I knew it, it was the tail end of 2024.

Ten glorious years!

So what's been going on? As I speak, numerous wars rage across the planet, and some of them even involve actual physical violence. Artificial Intelligence is being hyped to the heavens the same way Blockchain and the Metaverse were. The USA is being all dramatic about their upcoming Presidential Election.

In other words, same shit, different day. Business as usual, folks!

This blog has kept moving along even with the numerous changes in my life since it first started in 2014. It hasn't been a huge success but it doesn't need to be. Maintaining it has kept me sharp, sharper than if I'd spent my time playing video games and scrolling on TikTok.

Blogging has been slow this year. From the October of last year, I concentrated on moving house, and finally completed the move on the last day of 2023. Since then, it's been a period of settling in. In addition to that, my Lenovo died and I was scrambling for a replacement.

Web Tutorials

This year, I broke away from my bread-and-butter of HTML, CSS and JavaScript, and started making web tutorials in Python. Mostly I just didn't want to neglect the skills I picked up in 2021 and allow them to atrophy too much. So if you regularly check in to see what other cool stuff I have that you can jump into right away without needing to install an environment for it... tough luck I guess.

Been doing a bit more
of Python lately.

Along the way, though, I found time to do something in ReactJS. And the result was last month's web tutorial.

My work has begun incorporating Generative A.I for minor improvements. Nothing earth-shattering. Just trying to develop that muscle memory in the event that one day I'll really need it. And also so that on the occasions I write about A.I, I'll actually have some idea what I'm talking about.

Since I'm on the subject of web tutorials, some readers would have noticed that my Easter web tutorial was posted after Easter this year, as opposed to before. This was a total accident as I lost track. But come to think of it, why can't I do this? There's no good reason, for example, for a Valentine web tutorial to always be before Valentine's Day. Sometimes the occurence of Valentine's Day and Chinese New Year in the same month, makes this a logistical challenge. Thus, from this year forth, I'm giving myself the liberty of posting such web tutorials after the day.

Tech News

As mentioned earlier, A.I has been dominating my newsfeed regularly. The other tech news regular, unfortunately, is big tech layoffs. Just seems to be a thing these days. Thankfully I'm not in big tech. I'm a tech practitioner in the food and beverage industry. Not one of the high-earners to be honest, but I don't have a problem with staying employed. As of last week, I've been at the company four years now.

Getting laid off.

Elon Musk also appears on my feed from time to time for something stupid. It gets less interesting each time, thus I'm saving my limited supply of fucks for something more... fuck-worthy, I guess.

There's been plenty of news related to work or tech (sometimes both!) on the home front. Some were neither, to be fair, even though I managed to relate it to my professional experiences - such as with Singapore's Smoking Samsui Woman Controversy Through a Web Developer's Eyes.

All in all, I think I've gotten better about identifying what qualifies as tech news, or at least tying it in by viewing it through a tech lens.

Other Stuff

My ongoing quest to remove irrelevant tags in this blog is progressing well. In the space of a few months, I've combed through the tags from A to Z. Some of the tags I removed were Donald Trump and Hillary Clinton, because clickbaity as they can be, they're not tech personalities and that's what I want to keep this blog about. Arguably I should have started way before now, but ah well, hindsight and all.

I've started included more personal stuff on here. Much of my personal stuff is tied into my identity as a software dev, either directly or indirectly, so that's great. A lot of my life realizations were gleaned from writing software.

I'm chill now. Swear to
fuckin' God.

This has been ongoing for a couple years now, but I'm proud to report that my efforts to decrease the amount of profanity that I use on this blog, is paying off. At least, it has for this year. What's really needed is to sustain the effort for another year, and another, until it becomes second nature. The last thing I want is to come off as this perpetually raging old man.

I've improved on my ability to use A.I to generate images for this blog. It's been a year, and you'll probably see that the images I've generated are far less plastic. I hope. Plus, it's free. Since I'm not making a damn cent off this blog, I think it's fair.

Next

Greatest hits of 2024.

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.