Hace algún tiempo, me encontré con un servicio bastante fascinante, la API de Deck of Cards . Esta API maneja todo lo imaginable relacionado con el trabajo con barajas de cartas. Maneja la creación de un conjunto de cartas barajadas (que contiene uno o más mazos), repartiendo una carta (o cartas) e incluso reorganizando.
Aún mejor, incluye imágenes de tarjetas que puedes usar si no quieres encontrar las tuyas propias:
Es una API increíblemente llena de funciones y, lo mejor de todo, es completamente gratuita. No es necesario ni siquiera una llave. Conozco esta API desde hace un tiempo y he contemplado crear un juego de cartas con ella, pero me di cuenta de que los juegos pueden pasar rápidamente de simples a bastante complejos.
De hecho, mis amigos me recomendaron encarecidamente que no dedicara tiempo a esto y, sinceramente, probablemente tenían razón, pero tengo un largo historial de creación de demostraciones de código que no tienen sentido. ;)
Para mi demostración, seguí las siguientes reglas:
Inicialmente, el jugador y la computadora tienen una matriz que representa sus manos.
playerCards:[], pcCards:[],
El método deal
maneja la configuración de las manos para ambos jugadores:
async deal() { // first to player, then PC, then player, then PC this.playerCards.push(await this.drawCard()); // for the dealer, the first card is turned over let newcard = await this.drawCard(); newcard.showback = true; this.pcCards.push(newcard); this.playerCards.push(await this.drawCard()); this.pcCards.push(await this.drawCard()); },
Dos cosas a señalar. Primero, trato con el jugador, luego con la PC (o con el crupier, en cuanto al nombre, voy de un lado a otro), y luego de regreso. También modifico el objeto de resultado de la tarjeta para que tenga showback
establecido de modo que pueda representar el reverso de la tarjeta para el crupier.
Así es como se hace en HTML:
<div id="pcArea" class="cardArea"> <h3>Dealer</h3> <template x-for="card in pcCards"> <!-- todo: don't like the logic in template --> <img :src="card.showback?BACK_CARD:card.image" :title="card.showback?'':card.title"> </template> </div> <div id="playerArea" class="cardArea"> <h3>Player</h3> <template x-for="card in playerCards"> <img :src="card.image" :title="card.title"> </template> </div>
BACK_CARD
es simplemente una constante:
const BACK_CARD = "https://deckofcardsapi.com/static/img/back.png";
Entonces, en este punto, podría presionar la aplicación y obtener una mano de Blackjack:
En la parte inferior, usé un div para mostrar el estado actual:
Mi lógica era así:
Centrémonos primero en el jugador. Para acertar, simplemente añadimos una carta:
async hitMe() { this.hitMeDisabled = true; this.playerCards.push(await this.drawCard()); let count = this.getCount(this.playerCards); if(count.lowCount >= 22) { this.playerTurn = false; this.playerBusted = true; } this.hitMeDisabled = false; },
La verificación del busto fue un poco compleja. Desarrollé una función para obtener el 'conteo' de la mano, pero en Blackjack, los ases pueden ser 1 u 11.
Descubrí (y espero tener razón) que nunca se pueden tener dos ases 'altos', por lo que mi función devuelve un valor lowCount
y highCount
donde para la versión alta, si existe un As, se cuenta como 11, pero solo uno. Aquí está esa lógica:
getCount(hand) { /* For a hand, I return 2 values, a low value, where aces are considered 1s, and a high value, where aces are 11. Note that this fails to properly handle a case where I have 3 aces and could have a mix... although thinking about it, you can only have ONE ace at 11, so maybe the logic is: low == all aces at 1. high = ONE ace at 11. fixed! */ let result = {}; // first we will do low, all 1s let lowCount = 0; for(card of hand) { if(card.value === 'JACK' || card.value === 'KING' || card.value === 'QUEEN') lowCount+=10; else if(card.value === 'ACE') lowCount += 1; else lowCount += Number(card.value); //console.log(card); } //console.log('lowCount', lowCount); let highCount = 0; let oneAce = false; for(card of hand) { if(card.value === 'JACK' || card.value === 'KING' || card.value === 'QUEEN') highCount+=10; else if(card.value === 'ACE') { if(oneAce) highCount += 1; else { highCount += 10; oneAce = true; } } else highCount += Number(card.value); } //console.log('highCount', highCount); return { lowCount, highCount }; },
Si el jugador pasa, finalizamos el juego y dejamos que el usuario comience de nuevo. Si se mantienen firmes, es hora de que el crupier tome el relevo. Esa lógica era simple: golpear mientras estaba por debajo de 17 y pasarse o plantarse.
Para hacerlo un poco más emocionante, utilicé una función variable y asíncrona, delay
, para ralentizar las acciones del crupier para que puedas verlas en (más o menos) tiempo real. Aquí está la lógica del distribuidor:
async startDealer() { /* Idea is - I take a card everytime I'm < 17. so i check my hand, and do it, see if im going to stay or hit. if hit, i do a delay though so the game isn't instant. */ // really first, initial text this.pcText = 'The dealer begins their turn...'; await delay(DEALER_PAUSE); // first, a pause while we talk this.pcText = 'Let me show my hand...'; await delay(DEALER_PAUSE); // reveal my second card this.pcCards[0].showback = false; // what does the player have, we need the best under 22 let playerCount = this.getCount(this.playerCards); let playerScore = playerCount.lowCount; if(playerCount.highCount < 22) playerScore = playerCount.highCount; //console.log('dealer needs to beat', playerScore); // ok, now we're going to loop until i bust/win let dealerLoop = true; while(dealerLoop) { let count = this.getCount(this.pcCards); /* We are NOT doing 'soft 17', so 1 ace always count as 11 */ if(count.highCount <= 16) { this.pcText = 'Dealer draws a card...'; await delay(DEALER_PAUSE); this.pcCards.push(await this.drawCard()); } else if(count.highCount <= 21) { this.pcText = 'Dealer stays...'; await delay(DEALER_PAUSE); dealerLoop = false; this.pcTurn = false; if(count.highCount >= playerScore) this.pcWon = true; else this.playerWon = true; } else { dealerLoop = false; this.pcTurn = false; this.pcBusted = true; } } }
FYI, pcText
se usa en el área de estado blanca como una forma de configurar los mensajes del juego.
Y básicamente, eso es todo. Si quiere jugarlo usted mismo, consulte el CodePen a continuación y siéntase libre de bifurcarlo y agregar mejoras: