From 1bcf9de7adc8bef9a8a09a7281fdbb1a6ba6dca4 Mon Sep 17 00:00:00 2001 From: James Monger Date: Wed, 26 Oct 2022 13:39:58 +0100 Subject: [PATCH] feat: create and use @shoki/card-deck --- modules/@creature-chess/gamemode/package.json | 1 + .../gamemode/src/game/cardDeck.ts | 39 +++++---- .../src/game/player/playerGameDeckSaga.ts | 12 +-- modules/@shoki/card-deck/README.md | 82 +++++++++++++++++++ modules/@shoki/card-deck/example.ts | 29 +++++++ modules/@shoki/card-deck/index.js | 78 ++++++++++++++++++ modules/@shoki/card-deck/index.ts | 56 +++++++++++++ modules/@shoki/card-deck/package.json | 36 ++++++++ modules/@shoki/card-deck/tsconfig.json | 11 +++ yarn.lock | 33 ++++++++ 10 files changed, 356 insertions(+), 21 deletions(-) create mode 100644 modules/@shoki/card-deck/README.md create mode 100644 modules/@shoki/card-deck/example.ts create mode 100644 modules/@shoki/card-deck/index.js create mode 100644 modules/@shoki/card-deck/index.ts create mode 100644 modules/@shoki/card-deck/package.json create mode 100644 modules/@shoki/card-deck/tsconfig.json diff --git a/modules/@creature-chess/gamemode/package.json b/modules/@creature-chess/gamemode/package.json index 81da7dae2..d875da6bd 100644 --- a/modules/@creature-chess/gamemode/package.json +++ b/modules/@creature-chess/gamemode/package.json @@ -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", diff --git a/modules/@creature-chess/gamemode/src/game/cardDeck.ts b/modules/@creature-chess/gamemode/src/game/cardDeck.ts index 5c338cf76..c7ac364ad 100644 --- a/modules/@creature-chess/gamemode/src/game/cardDeck.ts +++ b/modules/@creature-chess/gamemode/src/game/cardDeck.ts @@ -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, @@ -37,10 +39,17 @@ const canTakeCardAtCost = (level: number, cost: number): boolean => { }; export class CardDeck { - public deck: Card[][]; + public decks: ShokiCardDeck[]; public constructor(private logger: Logger) { - this.deck = [[], [], [], [], []]; + // TODO (James) customisable number of decks + this.decks = [ + new ShokiCardDeck(), + new ShokiCardDeck(), + new ShokiCardDeck(), + new ShokiCardDeck(), + new ShokiCardDeck(), + ]; getAllDefinitions() .filter((d) => d.cost) @@ -54,7 +63,7 @@ export class CardDeck { } }); - this.shuffle(); + this.shuffleAllDecks(); } public reroll( @@ -64,7 +73,7 @@ export class CardDeck { excludeCards: number[] = [] ) { this.addCards(input); - this.shuffle(); + this.shuffleAllDecks(); return this.take(count, level, excludeCards); } @@ -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) { @@ -92,7 +101,7 @@ export class CardDeck { this.addDefinition(definition); } - this.shuffle(); + this.shuffleAllDecks(); } public addPieces(pieces: PieceModel[]) { @@ -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[] = []) { @@ -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)) { @@ -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)) { @@ -185,6 +194,6 @@ export class CardDeck { class: definition.class, }; - this.getDeckForCost(definition.cost).push(card); + this.getDeckForCost(definition.cost).addCards(card, false); } } diff --git a/modules/@creature-chess/gamemode/src/game/player/playerGameDeckSaga.ts b/modules/@creature-chess/gamemode/src/game/player/playerGameDeckSaga.ts index dd29093d5..3c36aa432 100644 --- a/modules/@creature-chess/gamemode/src/game/player/playerGameDeckSaga.ts +++ b/modules/@creature-chess/gamemode/src/game/player/playerGameDeckSaga.ts @@ -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(); @@ -38,11 +38,11 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) { deck.addPiece(piece); } deck.addCards(cards); - deck.shuffle(); + deck.shuffleAllDecks(); }; yield all([ - takeEvery(playerDeathEvent.toString(), function* () { + takeEvery(playerDeathEvent.toString(), function*() { const cards = yield* select(getPlayerCards); const pieces = yield* select(getAllPieces); @@ -68,7 +68,7 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) { }), takeEvery( afterRerollCardsEvent.toString(), - function* () { + function*() { const state = yield* select((s: PlayerState) => s); if (!isPlayerAlive(state)) { @@ -93,10 +93,10 @@ export const playerGameDeckSagaFactory = function* (deck: CardDeck) { ), takeEvery( 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(); } ), ]); diff --git a/modules/@shoki/card-deck/README.md b/modules/@shoki/card-deck/README.md new file mode 100644 index 000000000..a1d60402f --- /dev/null +++ b/modules/@shoki/card-deck/README.md @@ -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(); +``` + +### 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(); +``` diff --git a/modules/@shoki/card-deck/example.ts b/modules/@shoki/card-deck/example.ts new file mode 100644 index 000000000..bf2e474ed --- /dev/null +++ b/modules/@shoki/card-deck/example.ts @@ -0,0 +1,29 @@ +import { CardDeck } from "./index"; + +type Card = { + value: number; + suit: string; +}; + +const deck = new CardDeck(); + +// 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(); diff --git a/modules/@shoki/card-deck/index.js b/modules/@shoki/card-deck/index.js new file mode 100644 index 000000000..bb888ae61 --- /dev/null +++ b/modules/@shoki/card-deck/index.js @@ -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; diff --git a/modules/@shoki/card-deck/index.ts b/modules/@shoki/card-deck/index.ts new file mode 100644 index 000000000..7734d5a1b --- /dev/null +++ b/modules/@shoki/card-deck/index.ts @@ -0,0 +1,56 @@ +import shuffle from "lodash.shuffle"; + +/** + * A deck of cards + * + * @template TCard The type of card + * + * @author jameskmonger + */ +export class CardDeck { + private deck: TCard[]; + + public constructor(deck?: TCard[]) { + this.deck = deck || []; + } + + /** + * Take a number of cards from the top of the deck + * + * @param count The number of cards to take (default 1) + * @returns The cards taken + */ + public take(count?: 1): TCard; + public take(count: number): TCard[]; + public take(count: number = 1): TCard | TCard[] { + 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 + */ + public addCards(cards: TCard | TCard[], shouldShuffle: boolean = true) { + // TODO null check? + if (Array.isArray(cards)) { + this.deck.push(...cards); + } else { + this.deck.push(cards); + } + + if (shouldShuffle) { + this.shuffle(); + } + } + + /** + * Shuffle the deck using lodash.shuffle (Fisher-Yates) + */ + public shuffle() { + this.deck = shuffle(this.deck); + } +} diff --git a/modules/@shoki/card-deck/package.json b/modules/@shoki/card-deck/package.json new file mode 100644 index 000000000..01aa2cd1b --- /dev/null +++ b/modules/@shoki/card-deck/package.json @@ -0,0 +1,36 @@ +{ + "name": "@shoki/card-deck", + "version": "1.0.0", + "description": "TypeScript deck of cards", + "keywords": [ + "shoki", + "game", + "engine", + "cards", + "card deck", + "playing cards" + ], + "author": "James Monger ", + "homepage": "https://github.com/Jameskmonger/creature-chess#readme", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Jameskmonger/creature-chess.git" + }, + "scripts": { + "build": "yarn run -T tsc -p ./tsconfig.json", + "test": "yarn run -T jest --passWithNoTests" + }, + "bugs": { + "url": "https://github.com/Jameskmonger/creature-chess/issues" + }, + "dependencies": { + "lodash.shuffle": "^4.2.0" + }, + "devDependencies": { + "@types/lodash.shuffle": "^4.2.7" + } +} diff --git a/modules/@shoki/card-deck/tsconfig.json b/modules/@shoki/card-deck/tsconfig.json new file mode 100644 index 000000000..fd74291be --- /dev/null +++ b/modules/@shoki/card-deck/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "declaration": true + }, + "extends": "../../../tsconfig.json", + "include": [ + "./src/**/*", + "./src/*", + "./index.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 6b5fe0bb5..479b88281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2115,6 +2115,7 @@ __metadata: "@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 @@ -3589,6 +3590,15 @@ __metadata: languageName: unknown linkType: soft +"@shoki/card-deck@workspace:*, @shoki/card-deck@workspace:modules/@shoki/card-deck": + version: 0.0.0-use.local + resolution: "@shoki/card-deck@workspace:modules/@shoki/card-deck" + dependencies: + "@types/lodash.shuffle": ^4.2.7 + lodash.shuffle: ^4.2.0 + languageName: unknown + linkType: soft + "@shoki/engine@workspace:*, @shoki/engine@workspace:modules/@shoki/engine": version: 0.0.0-use.local resolution: "@shoki/engine@workspace:modules/@shoki/engine" @@ -5263,6 +5273,22 @@ __metadata: languageName: node linkType: hard +"@types/lodash.shuffle@npm:^4.2.7": + version: 4.2.7 + resolution: "@types/lodash.shuffle@npm:4.2.7" + dependencies: + "@types/lodash": "*" + checksum: b6417a358159982a93273a96ade01801923628bf4f288f686200501cae664d8d04deb492ec072b435a0837e6a2b671e1ae868aeaeb047242ff447fabec53d4a5 + languageName: node + linkType: hard + +"@types/lodash@npm:*": + version: 4.14.186 + resolution: "@types/lodash@npm:4.14.186" + checksum: ee0c1368a8100bb6efb88335107473a41928fc307ff1ef4ff1278868ccddba9c04c68c36d1ffe3a0392ef4a956e1955f7de3203ec09df4f1655dd1b88485c549 + languageName: node + linkType: hard + "@types/lodash@npm:^4.14.123, @types/lodash@npm:^4.14.167": version: 4.14.182 resolution: "@types/lodash@npm:4.14.182" @@ -15886,6 +15912,13 @@ __metadata: languageName: node linkType: hard +"lodash.shuffle@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.shuffle@npm:4.2.0" + checksum: 3d451e16c18cd7e7c4c265199c87f0a1fff2976b8bf14d94de20a445e76f61a5c39b62181e5660e8f3a1602518b15a3d7d54732067cdef9d446e47d1e8f48938 + languageName: node + linkType: hard + "lodash.truncate@npm:^4.4.2": version: 4.4.2 resolution: "lodash.truncate@npm:4.4.2"