No. Thank you and goodbye.
Ok, first off, I apologize for the click-bait style title. Every now and then, when I get an idea for a demo, it doesn't work. But sometimes, it doesn't work out in a fun and interesting way, and I figure it's a good idea to share it anyway. (Also, there's always the strong chance that it didn't work out because I did something wrong!) Today's demo is a perfect example of that, I think.
Earlier this year, I built a Blackjack game using Alpine.js and the quite excellent Deck of Cards API. Yesterday, I was thinking about the game, and I wondered - what would happen if I used generative AI to ask for help when playing it? So, for example, I went to my game and saw this:
In Google's PaLM Makersuite app, I wrote:
I'm playing blackjack and the dealer has a six of diamonds showing. I've got a jack of clubs and a jack of hearts. Should I hit or should I stay?
And got this response:
You have 20, which is a good hand. The dealer has 16, which is below the average. If you hit, you risk getting a card that will bust you. So it's better to stay and hope that the dealer busts.
Which, ok, is pretty obvious. You would never hit with two Jacks. But I loved the descriptive response that reinforces the principles of the game. I thought - what if I added a button to the game that lets you ask GenAI for help? Here's how I did it:
On the front end, I kept the UI changes pretty minimal. I began by adding a simple button:
Clicking this needs to do two important things. First, it needs to 'translate' the game state into a prompt; then, it needs to pass this to a service that will handle the gen AI call. I created a new function, askForHelp
, that handles it:
async askForHelp() {
this.aiHelp = '<i>Asking our AI friend...</i>';
// first, "translate" the cards to English
let q = `I'm playing blackjack and the dealer currently has ${this.pcCards[1].value} of ${this.pcCards[1].suit.toLowerCase()}. I've got a ${this.playerCards[0].value} of ${this.playerCards[0].suit.toLowerCase()} and a ${this.playerCards[1].value} of ${this.playerCards[1].suit.toLowerCase()}. Should I hit or should I stay?`;
console.log(q);
let resp = await fetch(`https://eo8dkukowewth66.m.pipedream.net?text=${encodeURIComponent(q)}`);
let help = await resp.json();
/*
note that help has line breaks and stuff in and in theory
we should map to br or something, but im fine ignoring.
*/
this.aiHelp = '';
alert(help);
},
I store the player and PC cards in two variables that contain an array of cards representing their hands. Each card has a 'value,’ which is either a number or a name, like Jack. Each card also has a suit. In theory, that's probably not critical information, but I figured it couldn't hurt. Here's an example prompt generated by a game:
I'm playing blackjack and the dealer currently has 8 of clubs. I've got a 7 of spades and a QUEEN of hearts. Should I hit or should I stay?
This is passed off to my backend service (more on that in a second), and the result is then displayed in a JavaScript alert, which is 100% horrible UX, but as this was an experiment, I figured it was good enough.
If you're curious, this is what the response was:
Stay. You have a total of 17, which is a good hand. The dealer's 8 is not a high card, so you are unlikely to lose if you stay.
For my back end, I whipped up a quick Pipedream workflow. My workflow consisted of literally just three steps.
The first is my trigger, an HTTP endpoint. You can see the URL in the front-end code above.
The second step is the built-in Google PaLM API action. All I had to do was tell it what to use for a prompt: {{steps.trigger.event.query.text}}
My final step simply returned the result:
export default defineComponent({
async run({ steps, $ }) {
await $.respond({
status: 200,
headers: {
"Content-Type":"application/json"
},
body: JSON.stringify(steps.generate_text.$return_value[0].candidates[0].output),
})
},
})
So... yeah. My first few tests worked great, and then... things went off the rails. Here's an example. The dealer had a seven of hearts. I had a queen of spades and an eight of diamonds. When I asked for help, I got:
The dealer's upcard is 7, which is a "hard" 17. That means that the dealer must stand regardless of what you do. You have a "soft" 19, which means that you have an ace that can count as either 1 or 11. You should stay.
Ok, sure, I should stay, but what in the actual frak? I have 19? The dealer has 17? And it gets better. Once when I had 18 showing, I got the following:
I would recommend hitting. With a total of 18, you are only one point away from 21, which is the winning hand in blackjack. The dealer's seven of hearts gives them a total of 17, which is below the 21 threshold. Therefore, there is a good chance that the dealer will bust if they hit, which would give you the win. However, if you stay, you will not be able to improve your hand and will have to hope that the dealer busts.
Hitting on 18. I'm no professional gambler, but I'm pretty sure that's bad advice.
This got me thinking about my prompt a bit more. I did some tweaks, like lowercasing the suit, as I was worried it may have confused the AI. I iterated through some options in Makersuite, and consistently, it seemed to struggle with basic math at times. Even when I was explicit:
I've got a jack of clubs (worth ten) and an eight of hearts (worth 8).
It still thought I had 19. I did have better luck with this:
I've got a jack of clubs (worth ten) and an eight of hearts (worth 8). My hand total is 18.
And I also tried this:
I've got a ace of clubs and an eight of hearts. My hand total is 9 or 19.
And that seemed to consistently work correctly. So, with that in mind, I went back to my prompt generation and updated my logic there. Here's the updated version:
async askForHelp() {
this.aiHelp = '<i>Asking our AI friend...</i>';
let totalOb = this.getCount(this.playerCards);
let totalStr = 'My hand has a total of ';
if(totalOb.lowCount === totalOb.highCount) {
totalStr += totalOb.lowCount;
} else {
totalStr += `${totalOb.lowCount} or ${totalOb.highCount}`;
}
// first, "translate" the cards to English
let q = `I'm playing blackjack and the dealer currently has ${this.pcCards[1].value} of ${this.pcCards[1].suit.toLowerCase()}. I've got a ${this.playerCards[0].value} of ${this.playerCards[0].suit.toLowerCase()} and a ${this.playerCards[1].value} of ${this.playerCards[1].suit.toLowerCase()}. ${totalStr}. Should I hit or should I stay?`;
console.log(q);
let resp = await fetch(`https://eo8dkukowewth66.m.pipedream.net?text=${encodeURIComponent(q)}`);
let help = await resp.json();
console.log('RESP', help);
/*
note that help has line breaks and stuff in and in theory
we should map to br or something, but im fine ignoring.
*/
this.aiHelp = '';
alert(help);
},
My Blackjack game has a utility function, getCount
, that returns an object containing two values lowCount
, where Aces are treated as one, and highCount
, where one Ace at most is considered eleven. With this, I add to my prompt information about the total.
This did help quite a bit, but I still sometimes returned delusional responses.
So... maybe consider it your drunk friend at the gambling table offering advice?
I'd love to share this version of the demo publicly, and I can share the code, of course, but I'm disabling the Pipedream workflow so that I don't get charged for it. Feel free to look at the front-end code here:
https://codepen.io/cfjedimaster/pen/QWYpQjY/905e31ed6d9e3539c041044b308f5f06?editors=1011
As always, let me know what you think!
Also published here.