Pick hole cards from a grid instead of typing them
Typing "AH" into a two-character box is fine at a desk and miserable at a table: it needs a keyboard, it accepts nonsense like "ZZ", and it happily deals the same card to two players. The two card fields become face-up card slots. Tapping one opens a 52-card grid — four suit columns, thirteen rank rows, one tap per card — and picking the first card moves straight on to the second, because a dealer picking hole cards is always picking two. Cards already dealt elsewhere at the table are greyed out and say who has them, so a duplicate is visibly out of play rather than silently accepted. Cards are still stored as the same two plain characters everything has always read; the suit only becomes a symbol on its way to a human. That formatting is now shared, so the table pucks, the seat editor, the pot wizard, the hand history and the CSV all say A♥ K♦ rather than three of them saying AH KD. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// Tap a card instead of typing it.
|
||||
//
|
||||
// Typing "AH" into a two-character box is fine at a desk and miserable at a table:
|
||||
// it needs a keyboard, it accepts nonsense like "ZZ", and it happily lets the same
|
||||
// card land in two players' hands. The picker is one tap per card, greys out cards
|
||||
// already dealt somewhere else at the table, and stores exactly the same two-character
|
||||
// string the rest of the tool has always read.
|
||||
const CardPicker = {
|
||||
RANKS: ["A", "K", "Q", "J", "T", "9", "8", "7", "6", "5", "4", "3", "2"],
|
||||
SUITS: [
|
||||
{ code: "S", symbol: "♠", red: false },
|
||||
{ code: "H", symbol: "♥", red: true },
|
||||
{ code: "D", symbol: "♦", red: true },
|
||||
{ code: "C", symbol: "♣", red: false },
|
||||
],
|
||||
|
||||
state: null,
|
||||
hooks: null,
|
||||
seatIndex: null,
|
||||
cardIndex: 0,
|
||||
|
||||
open(state, hooks, seatIndex, cardIndex) {
|
||||
this.state = state;
|
||||
this.hooks = hooks;
|
||||
this.seatIndex = seatIndex;
|
||||
this.cardIndex = cardIndex;
|
||||
this.render();
|
||||
document.getElementById("cardPickerOverlay").hidden = false;
|
||||
},
|
||||
|
||||
close() {
|
||||
document.getElementById("cardPickerOverlay").hidden = true;
|
||||
},
|
||||
|
||||
// Every card sitting in front of anyone else. The slot being edited is excluded,
|
||||
// so re-picking the card that's already there isn't treated as a duplicate.
|
||||
cardsInUse() {
|
||||
const used = new Map();
|
||||
this.state.seats.forEach((player, seat) => {
|
||||
(player.cards || []).forEach((card, slot) => {
|
||||
if (!card) return;
|
||||
if (seat === this.seatIndex && slot === this.cardIndex) return;
|
||||
used.set(card.toUpperCase(), HoldemState.displayName(player, seat));
|
||||
});
|
||||
});
|
||||
return used;
|
||||
},
|
||||
|
||||
assign(card) {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
player.cards[this.cardIndex] = card;
|
||||
this.hooks.onPick(this.seatIndex, this.cardIndex, card);
|
||||
|
||||
// Straight on to the second card — a dealer picking hole cards is always
|
||||
// picking two, and making them re-open the picker doubles the taps.
|
||||
if (this.cardIndex === 0 && !player.cards[1]) {
|
||||
this.cardIndex = 1;
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
|
||||
clearSlot() {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
player.cards[this.cardIndex] = "";
|
||||
this.hooks.onPick(this.seatIndex, this.cardIndex, "");
|
||||
this.render();
|
||||
},
|
||||
|
||||
render() {
|
||||
const player = this.state.seats[this.seatIndex];
|
||||
const used = this.cardsInUse();
|
||||
|
||||
document.getElementById("cardPickerTitle").textContent =
|
||||
`${HoldemState.displayName(player, this.seatIndex)} — card ${this.cardIndex + 1}`;
|
||||
|
||||
// Which slot the next tap fills, switchable without closing the sheet.
|
||||
const slots = document.getElementById("cardPickerSlots");
|
||||
slots.innerHTML = "";
|
||||
[0, 1].forEach(slot => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pickerSlot";
|
||||
btn.classList.toggle("active", slot === this.cardIndex);
|
||||
const value = player.cards[slot] || "";
|
||||
btn.textContent = value || "—";
|
||||
btn.classList.toggle("cardRed", /[HD]$/i.test(value));
|
||||
btn.addEventListener("click", () => {
|
||||
this.cardIndex = slot;
|
||||
this.render();
|
||||
});
|
||||
slots.appendChild(btn);
|
||||
});
|
||||
|
||||
const grid = document.getElementById("cardPickerGrid");
|
||||
grid.innerHTML = "";
|
||||
this.RANKS.forEach(rank => {
|
||||
this.SUITS.forEach(suit => {
|
||||
const card = `${rank}${suit.code}`;
|
||||
const takenBy = used.get(card);
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "pickerCard";
|
||||
btn.classList.toggle("cardRed", suit.red);
|
||||
btn.disabled = !!takenBy;
|
||||
if (takenBy) btn.title = `Already dealt to ${takenBy}`;
|
||||
if ((player.cards[this.cardIndex] || "").toUpperCase() === card) {
|
||||
btn.classList.add("selected");
|
||||
}
|
||||
|
||||
const rankEl = document.createElement("span");
|
||||
rankEl.className = "pickerRank";
|
||||
rankEl.textContent = rank;
|
||||
const suitEl = document.createElement("span");
|
||||
suitEl.className = "pickerSuit";
|
||||
suitEl.textContent = suit.symbol;
|
||||
btn.appendChild(rankEl);
|
||||
btn.appendChild(suitEl);
|
||||
|
||||
btn.addEventListener("click", () => this.assign(card));
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
module.exports = CardPicker;
|
||||
}
|
||||
@@ -144,6 +144,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
|
||||
|
||||
document.getElementById("cardPickerCloseBtn").addEventListener("click", () => CardPicker.close());
|
||||
document.getElementById("cardPickerClearBtn").addEventListener("click", () => CardPicker.clearSlot());
|
||||
document.getElementById("cardPickerOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "cardPickerOverlay") CardPicker.close();
|
||||
});
|
||||
|
||||
document.getElementById("closePotWizardBtn").addEventListener("click", () => PotWizard.close());
|
||||
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
||||
|
||||
@@ -38,7 +38,7 @@ const PotWizard = {
|
||||
|
||||
labelFor(player, seatIndex) {
|
||||
const name = HoldemState.displayName(player, seatIndex);
|
||||
const cards = (player.cards || []).filter(Boolean).join(" ");
|
||||
const cards = (player.cards || []).filter(Boolean).map(c => Render.formatCard(c)).join(" ");
|
||||
return cards ? `${name} (${cards})` : name;
|
||||
},
|
||||
|
||||
|
||||
@@ -132,6 +132,25 @@ const Render = {
|
||||
return /[HD]$/i.test(card || "");
|
||||
},
|
||||
|
||||
SUIT_SYMBOLS: { S: "♠", H: "♥", D: "♦", C: "♣" },
|
||||
|
||||
// "KD" -> "K♦". Stored cards stay two plain characters so every existing reader
|
||||
// keeps working; the suit only becomes a symbol on its way to a human.
|
||||
formatCard(card) {
|
||||
const value = (card || "").toUpperCase();
|
||||
if (!value) return "";
|
||||
return `${value[0]}${this.SUIT_SYMBOLS[value[1]] || value[1] || ""}`;
|
||||
},
|
||||
|
||||
// An empty slot has to look like something you can tap, so it gets a placeholder
|
||||
// rather than going blank.
|
||||
paintCardSlot(slot, card) {
|
||||
const value = (card || "").toUpperCase();
|
||||
slot.classList.toggle("cardRed", this.isRedSuit(value));
|
||||
slot.classList.toggle("cardSlotEmpty", !value);
|
||||
slot.textContent = value ? this.formatCard(value) : "+";
|
||||
},
|
||||
|
||||
// A chair nobody has claimed: no name typed, not dealt in, nothing in the pot.
|
||||
// Worth calling out, because at a short-handed table the open seats are what
|
||||
// the dealer is looking for.
|
||||
@@ -154,7 +173,7 @@ const Render = {
|
||||
if (i > 0) cardsEl.appendChild(document.createTextNode(" "));
|
||||
const span = document.createElement("span");
|
||||
if (this.isRedSuit(card)) span.className = "cardRed";
|
||||
span.textContent = card;
|
||||
span.textContent = this.formatCard(card);
|
||||
cardsEl.appendChild(span);
|
||||
});
|
||||
// What's in front of them this street reads as the live number during betting;
|
||||
@@ -288,17 +307,16 @@ const Render = {
|
||||
this.commitSeatChange(form, player, index);
|
||||
});
|
||||
|
||||
const card1 = form.querySelector(".card1Input");
|
||||
const card2 = form.querySelector(".card2Input");
|
||||
card1.value = player.cards[0] || "";
|
||||
card2.value = player.cards[1] || "";
|
||||
[card1, card2].forEach((input, cardIdx) => {
|
||||
input.classList.toggle("cardRed", this.isRedSuit(input.value));
|
||||
input.addEventListener("input", () => {
|
||||
input.value = input.value.toUpperCase();
|
||||
input.classList.toggle("cardRed", this.isRedSuit(input.value));
|
||||
player.cards[cardIdx] = input.value;
|
||||
this.commitSeatChange(form, player, index);
|
||||
const slots = [form.querySelector(".card1Slot"), form.querySelector(".card2Slot")];
|
||||
slots.forEach((slot, cardIdx) => {
|
||||
this.paintCardSlot(slot, player.cards[cardIdx]);
|
||||
slot.addEventListener("click", () => {
|
||||
CardPicker.open(this.state, {
|
||||
onPick: (seatIndex, pickedIdx, card) => {
|
||||
this.paintCardSlot(slots[pickedIdx], card);
|
||||
this.commitSeatChange(form, player, seatIndex);
|
||||
},
|
||||
}, index, cardIdx);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user