Skip to content

Commit

Permalink
feat: create and use @shoki/card-deck
Browse files Browse the repository at this point in the history
  • Loading branch information
Jameskmonger committed Oct 26, 2022
1 parent 1293aaa commit 1bcf9de
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 21 deletions.
1 change: 1 addition & 0 deletions modules/@creature-chess/gamemode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@creature-chess/models": "workspace:*",
"@reduxjs/toolkit": "^1.7.1",
"@shoki/board": "workspace:*",
"@shoki/card-deck": "workspace:*",
"@shoki/engine": "workspace:*",
"@types/lodash": "^4.14.178",
"@types/uuid": "^8.3.3",
Expand Down
39 changes: 24 additions & 15 deletions modules/@creature-chess/gamemode/src/game/cardDeck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { shuffle } from "lodash";
import { v4 as uuid } from "uuid";
import { Logger } from "winston";

import { CardDeck as ShokiCardDeck } from "@shoki/card-deck";

import {
CreatureDefinition,
Card,
Expand Down Expand Up @@ -37,10 +39,17 @@ const canTakeCardAtCost = (level: number, cost: number): boolean => {
};

export class CardDeck {
public deck: Card[][];
public decks: ShokiCardDeck<Card>[];

public constructor(private logger: Logger) {
this.deck = [[], [], [], [], []];
// TODO (James) customisable number of decks
this.decks = [
new ShokiCardDeck<Card>(),
new ShokiCardDeck<Card>(),
new ShokiCardDeck<Card>(),
new ShokiCardDeck<Card>(),
new ShokiCardDeck<Card>(),
];

getAllDefinitions()
.filter((d) => d.cost)
Expand All @@ -54,7 +63,7 @@ export class CardDeck {
}
});

this.shuffle();
this.shuffleAllDecks();
}

public reroll(
Expand All @@ -64,7 +73,7 @@ export class CardDeck {
excludeCards: number[] = []
) {
this.addCards(input);
this.shuffle();
this.shuffleAllDecks();

return this.take(count, level, excludeCards);
}
Expand All @@ -73,10 +82,10 @@ export class CardDeck {
const cardsToAdd = cards.filter((card) => card !== null);

for (const card of cardsToAdd) {
this.getDeckForCost(card.cost).push(card);
this.getDeckForCost(card.cost).addCards(card, false);
}

this.shuffle();
this.shuffleAllDecks();
}

public addPiece(piece: PieceModel) {
Expand All @@ -92,7 +101,7 @@ export class CardDeck {
this.addDefinition(definition);
}

this.shuffle();
this.shuffleAllDecks();
}

public addPieces(pieces: PieceModel[]) {
Expand All @@ -101,14 +110,14 @@ export class CardDeck {
}
}

public shuffle() {
for (let i = 0; i < this.deck.length; i++) {
this.deck[i] = shuffle(this.deck[i]);
public shuffleAllDecks() {
for (const deck of this.decks) {
deck.shuffle();
}
}

private getDeckForCost(cost: number): Card[] {
return this.deck[cost - 1];
private getDeckForCost(cost: number) {
return this.decks[cost - 1];
}

private take(count: number, level: number, excludeCards: number[] = []) {
Expand Down Expand Up @@ -139,7 +148,7 @@ export class CardDeck {
// try 3 times to get a non-excluded card
// todo rethink this as below
for (let i = 0; i < 3; i++) {
const card = this.getDeckForCost(cost).pop();
const card = this.getDeckForCost(cost).take();

if (card) {
if (!excludeDefinitions.includes(card.definitionId)) {
Expand All @@ -158,7 +167,7 @@ export class CardDeck {
// try 3 times to get a non-excluded card
// todo rethink this as above
for (let i = 0; i < 3; i++) {
const card = this.getDeckForCost(cost).pop();
const card = this.getDeckForCost(cost).take();

if (card) {
if (!excludeDefinitions.includes(card.definitionId)) {
Expand All @@ -185,6 +194,6 @@ export class CardDeck {
class: definition.class,
};

this.getDeckForCost(definition.cost).push(card);
this.getDeckForCost(definition.cost).addCards(card, false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { getAllPieces } from "../../player/pieceSelectors";
import { CardDeck } from "../cardDeck";

export const playerGameDeckSagaFactory = function* (deck: CardDeck) {
export const playerGameDeckSagaFactory = function*(deck: CardDeck) {
const boardSlice = yield* getBoardSlice();
const benchSlice = yield* getBenchSlice();

Expand All @@ -38,11 +38,11 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) {
deck.addPiece(piece);
}
deck.addCards(cards);
deck.shuffle();
deck.shuffleAllDecks();
};

yield all([
takeEvery<PlayerDeathEvent>(playerDeathEvent.toString(), function* () {
takeEvery<PlayerDeathEvent>(playerDeathEvent.toString(), function*() {
const cards = yield* select(getPlayerCards);
const pieces = yield* select(getAllPieces);

Expand All @@ -68,7 +68,7 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) {
}),
takeEvery<AfterRerollCardsEvent>(
afterRerollCardsEvent.toString(),
function* () {
function*() {
const state = yield* select((s: PlayerState) => s);

if (!isPlayerAlive(state)) {
Expand All @@ -93,10 +93,10 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) {
),
takeEvery<AfterSellPieceEvent>(
afterSellPieceEvent.toString(),
function* ({ payload: { piece } }) {
function*({ payload: { piece } }) {
// when a player sells a piece, add it back to the deck
deck.addPiece(piece);
deck.shuffle();
deck.shuffleAllDecks();
}
),
]);
Expand Down
82 changes: 82 additions & 0 deletions modules/@shoki/card-deck/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# card-deck

A TypeScript card deck implementation, with shuffling using the [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle).

## Installation

```
npm i @shoki/card-deck
```

or

```
yarn add @shoki/card-deck
```

## Usage

### Creating a deck

When creating a deck, you must provide a generic parameter `TCard`. This indicates the type of card within the deck.

```ts
import { CardDeck } from "@shoki/card-deck";

type Card = {
value: number;
suit: string;
};

const deck = new CardDeck<Card>();
```

### Adding cards

You can add a card, or cards, to the deck with the `addCards` function.

There is an optional boolean parameter `shouldShuffle` (which defaults to `true`) to indicate whether the deck should be shuffled (with the `shuffle` function described below) immediately after the addition.

```ts
// Add a single card
deck.addCards({ value: 1, suit: "hearts" });

// Add a card without shuffling
deck.addCards({ value: 2, suit: "hearts" }, false);

// Add multiple cards
deck.addCards([
{ value: 3, suit: "hearts" },
{ value: 4, suit: "hearts" },
]);

// Add multiple cards without shuffling
deck.addCards(
[
{ value: 3, suit: "hearts" },
{ value: 4, suit: "hearts" },
],
false
);
```

### Taking cards

You can take a card, or multiple cards, from the deck with the `take` function.

```ts
// Take a single card
const card = deck.take();

// Take multiple cards
const cards = deck.take(2);
```

### Shuffling the deck

This uses the [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) through [`lodash.shuffle`](https://www.npmjs.com/package/lodash.shuffle).

```ts
// Shuffle the deck
deck.shuffle();
```
29 changes: 29 additions & 0 deletions modules/@shoki/card-deck/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { CardDeck } from "./index";

type Card = {
value: number;
suit: string;
};

const deck = new CardDeck<Card>();

// Add a single card
deck.addCards({ value: 1, suit: "hearts" });

// Add a card without shuffling
deck.addCards({ value: 2, suit: "hearts" }, false);

// Add multiple cards
deck.addCards([
{ value: 3, suit: "hearts" },
{ value: 4, suit: "hearts" },
]);

// Take a single card
const card = deck.take();

// Take multiple cards
const cards = deck.take(2);

// Shuffle the deck
deck.shuffle();
78 changes: 78 additions & 0 deletions modules/@shoki/card-deck/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"use strict";
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
exports.CardDeck = void 0;
var lodash_shuffle_1 = __importDefault(require("lodash.shuffle"));
/**
* A deck of cards
*
* @template TCard The type of card
*
* @author jameskmonger
*/
var CardDeck = /** @class */ (function () {
function CardDeck(deck) {
this.deck = deck || [];
}
CardDeck.prototype.take = function (count) {
if (count === void 0) { count = 1; }
return this.deck.splice(this.deck.length - count, count);
};
/**
* Add a number of cards to the top of the deck
*
* Shuffles the deck after adding by default (see `shouldShuffle` parameter)
*
* @param cards The cards to add
* @param shouldShuffle Whether to shuffle the deck after adding
*/
CardDeck.prototype.addCards = function (cards, shouldShuffle) {
var _a;
if (shouldShuffle === void 0) { shouldShuffle = true; }
// TODO null check?
if (Array.isArray(cards)) {
(_a = this.deck).push.apply(_a, __spreadArray([], __read(cards), false));
}
else {
this.deck.push(cards);
}
if (shouldShuffle) {
this.shuffle();
}
};
/**
* Shuffle the deck using lodash.shuffle (Fisher-Yates)
*/
CardDeck.prototype.shuffle = function () {
this.deck = (0, lodash_shuffle_1["default"])(this.deck);
};
return CardDeck;
}());
exports.CardDeck = CardDeck;
Loading

0 comments on commit 1bcf9de

Please sign in to comment.