layout blackjack table
This commit is contained in:
88
client/src/Animation.ts
Normal file
88
client/src/Animation.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import type { Sprite, Ticker, TickerCallback } from "pixi.js";
|
||||||
|
import { CardStatus } from "./Hand";
|
||||||
|
import { getSpriteSheet } from "./spritesheet";
|
||||||
|
|
||||||
|
const spritesheet = await getSpriteSheet();
|
||||||
|
|
||||||
|
export abstract class Animation {
|
||||||
|
protected ticker: Ticker;
|
||||||
|
protected cb: TickerCallback<any>;
|
||||||
|
#next: Animation | null;
|
||||||
|
|
||||||
|
constructor(ticker: Ticker) {
|
||||||
|
console.log("this got called", ticker);
|
||||||
|
if (ticker === undefined) throw new Error("got nothing");
|
||||||
|
this.ticker = ticker;
|
||||||
|
this.cb = () => {};
|
||||||
|
this.#next = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract getCallback(): TickerCallback<any>;
|
||||||
|
|
||||||
|
next(animation: Animation) {
|
||||||
|
this.#next = animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
done() {
|
||||||
|
console.log("removing", this.ticker);
|
||||||
|
this.ticker.remove(this.cb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FlipCard extends Animation {
|
||||||
|
card: Sprite;
|
||||||
|
cardWidth: number;
|
||||||
|
status: CardStatus;
|
||||||
|
flipped: boolean;
|
||||||
|
|
||||||
|
constructor(ticker: Ticker, card: Sprite, status: CardStatus) {
|
||||||
|
super(ticker);
|
||||||
|
|
||||||
|
this.cb = () => {};
|
||||||
|
this.card = card;
|
||||||
|
this.status = status;
|
||||||
|
this.flipped = false;
|
||||||
|
this.cardWidth = card.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCallback() {
|
||||||
|
const callback = (ticker: Ticker) => {
|
||||||
|
const to = this.status.isFaceDown ? this.status.face : "unknown";
|
||||||
|
this.status.isFaceDown = !this.status.isFaceDown;
|
||||||
|
|
||||||
|
console.log(this);
|
||||||
|
|
||||||
|
this.card.pivot.set(this.cardWidth / 2, 0);
|
||||||
|
|
||||||
|
// the card has just fipped
|
||||||
|
if (!this.flipped && this.card.width <= 10) {
|
||||||
|
this.flipped = true;
|
||||||
|
this.card.texture = spritesheet.textures[to];
|
||||||
|
}
|
||||||
|
|
||||||
|
// the other side of the card is how showing
|
||||||
|
if (this.flipped) {
|
||||||
|
const change = 20 * ticker.deltaTime;
|
||||||
|
if (this.card.width + change >= this.cardWidth)
|
||||||
|
this.card.width = this.cardWidth;
|
||||||
|
else this.card.width += change;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the original side is still showing
|
||||||
|
else {
|
||||||
|
const change = 20 * ticker.deltaTime;
|
||||||
|
if (this.card.width <= change) this.card.width = 0;
|
||||||
|
else this.card.width -= change;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.flipped && this.card.width === this.cardWidth) {
|
||||||
|
console.log("remove", this.flipped, this.card.width, this.cardWidth);
|
||||||
|
super.done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.cb = callback.bind(this);
|
||||||
|
|
||||||
|
return this.cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,53 @@
|
|||||||
import { Assets, Container, Sprite } from "pixi.js";
|
import {
|
||||||
|
Assets,
|
||||||
|
Container,
|
||||||
|
Sprite,
|
||||||
|
Spritesheet,
|
||||||
|
Ticker,
|
||||||
|
type TickerCallback,
|
||||||
|
} from "pixi.js";
|
||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
import { CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||||
|
import { Animation, FlipCard } from "./Animation";
|
||||||
|
|
||||||
type Pivot = "left" | "right" | "center";
|
type Pivot = "left" | "right" | "center";
|
||||||
|
type Layout = "horizontal" | "ascending" | "stacked";
|
||||||
|
|
||||||
const spritesheet = await Assets.load("/public/assets/cards.json");
|
const spritesheet = await Assets.load<Spritesheet>("/public/assets/cards.json");
|
||||||
|
export type CardStatus = {
|
||||||
|
face: Card;
|
||||||
|
isAnimating: boolean;
|
||||||
|
isFaceDown: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export class Hand {
|
export class Hand {
|
||||||
#cards: Sprite[];
|
#animations: Animation[];
|
||||||
|
#cardSprites: Sprite[];
|
||||||
|
#statuses: CardStatus[];
|
||||||
#container: Container;
|
#container: Container;
|
||||||
|
#hover: boolean;
|
||||||
|
#layout: Layout;
|
||||||
#maxWidth: number;
|
#maxWidth: number;
|
||||||
#pivot: Pivot;
|
#pivot: Pivot;
|
||||||
|
#ticker: Ticker | null;
|
||||||
|
|
||||||
constructor(maxWidth: number, cards: Card[] = []) {
|
constructor(maxWidth: number, cards: Card[] = []) {
|
||||||
if (maxWidth < CARD_WIDTH) {
|
if (maxWidth < CARD_WIDTH) {
|
||||||
throw new Error("hand cannot be narrower than a single card");
|
throw new Error("hand cannot be narrower than a single card");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#maxWidth = maxWidth;
|
this.#animations = [];
|
||||||
this.#container = new Container();
|
this.#container = new Container();
|
||||||
|
this.#hover = false;
|
||||||
|
this.#maxWidth = maxWidth;
|
||||||
this.#pivot = "left";
|
this.#pivot = "left";
|
||||||
|
this.#layout = "horizontal";
|
||||||
|
this.#statuses = cards.map((card) => ({
|
||||||
|
face: card,
|
||||||
|
isAnimating: false,
|
||||||
|
isFaceDown: false,
|
||||||
|
}));
|
||||||
|
this.#ticker = null;
|
||||||
|
|
||||||
if (cards.length > 0) {
|
if (cards.length > 0) {
|
||||||
const sprites: Sprite[] = [];
|
const sprites: Sprite[] = [];
|
||||||
@ -27,33 +55,90 @@ export class Hand {
|
|||||||
this.add(card);
|
this.add(card);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#cards = sprites;
|
this.#cardSprites = sprites;
|
||||||
this.#container.addChild(...sprites);
|
this.#container.addChild(...sprites);
|
||||||
this.fanCards();
|
this.fanCards();
|
||||||
} else {
|
} else {
|
||||||
this.#cards = [];
|
this.#cardSprites = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add(card: Card) {
|
add(card: Card, faceDown = false) {
|
||||||
const sprite = new Sprite(spritesheet.textures[card]);
|
const sprite = new Sprite(spritesheet.textures[card]);
|
||||||
sprite.eventMode = "dynamic";
|
|
||||||
sprite.onmouseenter = () => {
|
this.#cardSprites.push(sprite);
|
||||||
sprite.y -= CARD_HOVER_DIST;
|
this.#statuses.push({
|
||||||
};
|
face: card,
|
||||||
sprite.onmouseleave = () => {
|
isAnimating: false,
|
||||||
sprite.y += CARD_HOVER_DIST;
|
isFaceDown: faceDown,
|
||||||
};
|
});
|
||||||
this.#cards.push(sprite);
|
|
||||||
this.#container.addChild(sprite);
|
this.#container.addChild(sprite);
|
||||||
|
this.#setCardHover(sprite);
|
||||||
this.fanCards();
|
this.fanCards();
|
||||||
this.setPivot(this.#pivot);
|
this.setPivot(this.#pivot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
animationTicker(ticker: Ticker) {
|
||||||
|
this.#ticker = ticker;
|
||||||
|
|
||||||
|
ticker.add((ticker) => {
|
||||||
|
if (this.#animations.length) {
|
||||||
|
const animation = this.#animations.shift()!;
|
||||||
|
ticker.add(animation.getCallback());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
flip(cardIdx: number) {
|
||||||
|
if (cardIdx >= this.#cardSprites.length)
|
||||||
|
throw new Error(`card index out of range: ${cardIdx}`);
|
||||||
|
|
||||||
|
if (!this.#ticker)
|
||||||
|
throw new Error("can't animate hand before passing it a ticker");
|
||||||
|
|
||||||
|
const card = this.#cardSprites[cardIdx];
|
||||||
|
const status = this.#statuses[cardIdx];
|
||||||
|
const ticker = this.#ticker;
|
||||||
|
|
||||||
|
this.#animations.push(new FlipCard(ticker, card, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
#setCardHover(sprite: Sprite) {
|
||||||
|
if (this.#hover) {
|
||||||
|
const { y } = sprite;
|
||||||
|
|
||||||
|
sprite.eventMode = "dynamic";
|
||||||
|
|
||||||
|
sprite.onmouseenter = () => {
|
||||||
|
sprite.y -= CARD_HOVER_DIST;
|
||||||
|
};
|
||||||
|
|
||||||
|
sprite.onmouseleave = () => {
|
||||||
|
sprite.y = y;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
sprite.eventMode = "static";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hover(hasHover: boolean) {
|
||||||
|
if (this.#hover === hasHover) return;
|
||||||
|
|
||||||
|
this.#hover = hasHover;
|
||||||
|
|
||||||
|
for (const sprite of this.#cardSprites) {
|
||||||
|
this.#setCardHover(sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setPosition(x: number, y: number) {
|
setPosition(x: number, y: number) {
|
||||||
this.#container.position.set(x, y);
|
this.#container.position.set(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCardLayout(layout: Layout) {
|
||||||
|
this.#layout = layout;
|
||||||
|
}
|
||||||
|
|
||||||
setPivot(pivot: "left" | "right" | "center") {
|
setPivot(pivot: "left" | "right" | "center") {
|
||||||
switch (pivot) {
|
switch (pivot) {
|
||||||
case "left":
|
case "left":
|
||||||
@ -73,13 +158,30 @@ export class Hand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fanCards() {
|
fanCards() {
|
||||||
const count = this.#cards.length;
|
const count = this.#cardSprites.length;
|
||||||
const max = this.#maxWidth;
|
const max = this.#maxWidth;
|
||||||
const offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
let offset = CARD_WIDTH / 3;
|
||||||
|
|
||||||
|
if (this.#layout === "horizontal") {
|
||||||
|
offset = count * CARD_WIDTH > max ? max / count : CARD_WIDTH;
|
||||||
|
}
|
||||||
let x = 0;
|
let x = 0;
|
||||||
for (const card of this.#cards) {
|
let y = 0;
|
||||||
|
for (const card of this.#cardSprites) {
|
||||||
card.x = x;
|
card.x = x;
|
||||||
x += offset;
|
card.y = y;
|
||||||
|
switch (this.#layout) {
|
||||||
|
case "ascending":
|
||||||
|
x += offset;
|
||||||
|
y -= offset;
|
||||||
|
break;
|
||||||
|
case "horizontal":
|
||||||
|
x += offset;
|
||||||
|
break;
|
||||||
|
case "stacked":
|
||||||
|
x += CARD_WIDTH / 20;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import { Application } from "pixi.js";
|
import {
|
||||||
|
Application,
|
||||||
|
Bounds,
|
||||||
|
FillGradient,
|
||||||
|
Graphics,
|
||||||
|
Text,
|
||||||
|
TextStyle,
|
||||||
|
TextStyleOptions,
|
||||||
|
} from "pixi.js";
|
||||||
import { Hand } from "./Hand";
|
import { Hand } from "./Hand";
|
||||||
import { Card } from "./Card";
|
import { Card } from "./Card";
|
||||||
import { CARD_HEIGHT, CARD_HOVER_DIST } from "./constants";
|
import { CARD_HEIGHT, CARD_HOVER_DIST, CARD_WIDTH } from "./constants";
|
||||||
|
|
||||||
const playerCards: Card[] = [
|
const playerCards: Card[] = ["nineOfSpades", "sixOfClubs"];
|
||||||
"threeOfClubs",
|
|
||||||
"twoOfDiamonds",
|
|
||||||
"aceOfClubs",
|
|
||||||
"eightOfDiamonds",
|
|
||||||
"nineOfSpades",
|
|
||||||
"sixOfClubs",
|
|
||||||
];
|
|
||||||
|
|
||||||
const dealerCards: Card[] = ["aceOfHearts"];
|
const dealerCards: Card[] = ["unknown", "aceOfHearts"];
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
@ -27,25 +28,108 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
|||||||
// Append the application canvas to the document body
|
// Append the application canvas to the document body
|
||||||
document.getElementById("pixi-container")!.appendChild(app.canvas);
|
document.getElementById("pixi-container")!.appendChild(app.canvas);
|
||||||
|
|
||||||
|
const payoutStyle: TextStyleOptions = {
|
||||||
|
fontFamily: "Fira Sans",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: CARD_HEIGHT / 5,
|
||||||
|
fill: "#ff9900",
|
||||||
|
padding: 10,
|
||||||
|
letterSpacing: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const blackjackPayout = new Text({
|
||||||
|
text: "BLACKJACK PAYS 3 TO 2",
|
||||||
|
style: payoutStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableMarkings = new Graphics();
|
||||||
|
|
||||||
|
const markTable = () => {
|
||||||
|
tableMarkings.clear();
|
||||||
|
|
||||||
|
// dark green dealer area
|
||||||
|
tableMarkings.rect(0, 0, app.screen.width, CARD_HEIGHT * 1.8);
|
||||||
|
tableMarkings.fill("#055010");
|
||||||
|
|
||||||
|
// white insurance info box
|
||||||
|
tableMarkings.rect(
|
||||||
|
CARD_WIDTH / 2,
|
||||||
|
CARD_HEIGHT * 2.15,
|
||||||
|
app.screen.width - CARD_WIDTH,
|
||||||
|
CARD_HEIGHT / 2.5,
|
||||||
|
);
|
||||||
|
tableMarkings.stroke({
|
||||||
|
color: "#ffffff",
|
||||||
|
width: 8,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionPayoutInfo = () => {
|
||||||
|
blackjackPayout.pivot.set(blackjackPayout.width / 2, 0);
|
||||||
|
blackjackPayout.position.set(app.screen.width / 2, CARD_HEIGHT * 1.5);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dealerRulesStyle: TextStyleOptions = {
|
||||||
|
fontFamily: "Fira Sans",
|
||||||
|
fontStyle: "italic",
|
||||||
|
fontSize: CARD_HEIGHT / 7,
|
||||||
|
fill: "#ffffff",
|
||||||
|
};
|
||||||
|
|
||||||
|
const dealerRules = new Text({
|
||||||
|
text: "Dealer must stand on 17 and must draw to 16",
|
||||||
|
style: dealerRulesStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const positionDealerInfo = () => {
|
||||||
|
dealerRules.pivot.set(dealerRules.width / 2, 0);
|
||||||
|
dealerRules.position.set(app.screen.width / 2, CARD_HEIGHT * 1.85);
|
||||||
|
};
|
||||||
|
|
||||||
|
const insuranceStyle: TextStyleOptions = {
|
||||||
|
fontFamily: "Fira Sans",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: CARD_HEIGHT / 5,
|
||||||
|
fill: "#ffffff",
|
||||||
|
letterSpacing: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const insurancePayout = new Text({
|
||||||
|
text: "INSURANCE PAYS 2 TO 1",
|
||||||
|
style: insuranceStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
const positionInsurancePayout = () => {
|
||||||
|
insurancePayout.pivot.set(insurancePayout.width / 2, 0);
|
||||||
|
insurancePayout.position.set(app.screen.width / 2, CARD_HEIGHT * 2.2);
|
||||||
|
};
|
||||||
|
|
||||||
const playerHand = new Hand(350, []);
|
const playerHand = new Hand(350, []);
|
||||||
const dealerHand = new Hand(350, []);
|
const dealerHand = new Hand(350, []);
|
||||||
|
|
||||||
const positionPlayer = () => {
|
const positionPlayer = () => {
|
||||||
playerHand.setPivot("center");
|
playerHand.setPivot("center");
|
||||||
playerHand.setPosition(
|
playerHand.setPosition(app.screen.width / 2, CARD_HEIGHT * 3);
|
||||||
app.screen.width / 2,
|
playerHand.setCardLayout("ascending");
|
||||||
app.screen.height - CARD_HEIGHT + CARD_HOVER_DIST,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const positionDealer = () => {
|
const positionDealer = () => {
|
||||||
dealerHand.setPivot("center");
|
dealerHand.setPivot("center");
|
||||||
dealerHand.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
dealerHand.setPosition(app.screen.width / 2, 0 + CARD_HOVER_DIST);
|
||||||
|
dealerHand.setCardLayout("stacked");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
markTable();
|
||||||
|
positionPayoutInfo();
|
||||||
|
positionDealerInfo();
|
||||||
|
positionInsurancePayout();
|
||||||
positionPlayer();
|
positionPlayer();
|
||||||
positionDealer();
|
positionDealer();
|
||||||
|
|
||||||
|
app.stage.addChild(tableMarkings);
|
||||||
|
app.stage.addChild(blackjackPayout);
|
||||||
|
app.stage.addChild(dealerRules);
|
||||||
|
app.stage.addChild(insurancePayout);
|
||||||
app.stage.addChild(playerHand.getContainer());
|
app.stage.addChild(playerHand.getContainer());
|
||||||
app.stage.addChild(dealerHand.getContainer());
|
app.stage.addChild(dealerHand.getContainer());
|
||||||
|
|
||||||
@ -60,6 +144,10 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
|||||||
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
// https://pixijs.com/8.x/guides/concepts/scene-graph#local-vs-global-coordinates
|
||||||
window.addEventListener?.("resize", () => {
|
window.addEventListener?.("resize", () => {
|
||||||
app.renderer.resize(window.innerWidth, window.innerHeight);
|
app.renderer.resize(window.innerWidth, window.innerHeight);
|
||||||
|
markTable();
|
||||||
|
positionPayoutInfo();
|
||||||
|
positionDealerInfo();
|
||||||
|
positionInsurancePayout();
|
||||||
positionPlayer();
|
positionPlayer();
|
||||||
positionDealer();
|
positionDealer();
|
||||||
});
|
});
|
||||||
@ -71,11 +159,11 @@ const dealerCards: Card[] = ["aceOfHearts"];
|
|||||||
if (elapsed >= ms) {
|
if (elapsed >= ms) {
|
||||||
ms += 300;
|
ms += 300;
|
||||||
if (playerCards.length) {
|
if (playerCards.length) {
|
||||||
playerHand.add(playerCards.pop()!);
|
playerHand.add(playerCards.shift()!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dealerCards.length) {
|
if (dealerCards.length) {
|
||||||
dealerHand.add(dealerCards.pop()!);
|
dealerHand.add(dealerCards.shift()!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
11
client/src/spritesheet.ts
Normal file
11
client/src/spritesheet.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Assets, Spritesheet } from "pixi.js";
|
||||||
|
|
||||||
|
let spriteSheet: Spritesheet | null = null;
|
||||||
|
|
||||||
|
export async function getSpriteSheet(): Promise<Spritesheet> {
|
||||||
|
if (spriteSheet === null) {
|
||||||
|
spriteSheet = await Assets.load<Spritesheet>("public/assets/cards.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
return spriteSheet;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user