diff --git a/src/components/board.ts b/src/components/board.ts index f27a182..bd422c2 100644 --- a/src/components/board.ts +++ b/src/components/board.ts @@ -1,37 +1,53 @@ -import { html } from 'lit-html' -import { atom } from 'nanostores' +import { html, nothing } from 'lit-html' +import { atom, computed, effect } from 'nanostores' -import { createCard } from '#core/card.ts' +import { watch } from '#core/lit-nanostores/watch.ts' import type { GameCard } from '#lobo/card.ts' +import { attack, endRound, startGame } from '#store/actions.ts' +import { $hasGameEnded, $isValidAttack } from '#store/derived.ts' +import { + $library, + $playerHand, + $selectedPlayerCardIds, + $selectedWolfCardIds, + $wolfHand, +} from '#store/state.ts' import { Card } from './card' import { Hand } from './hand' export function Board() { - const nextCard = atom(createCard(1)) - - return html`
) => { - console.log('card:click', event.detail) - }} - > -
${Card(nextCard)}
+ const $nextCard = atom(null) + const $nextCardRender = computed($nextCard, (nextCard) => (nextCard ? Card(nextCard) : nothing)) + + const $disableAttack = computed($isValidAttack, (valid) => (valid ? nothing : true)) + const $disableEndRound = computed($hasGameEnded, (ended) => (ended ? true : nothing)) + + effect($library, (library) => { + if (!library) { + $nextCard.set(null) + return + } + + const onDraw = () => $nextCard.set(library.peek()) + onDraw() + library.addEventListener('draw', onDraw) + return () => library.removeEventListener('draw', onDraw) + }) + + startGame('easy') + + return html`
+
${watch($nextCardRender)}
- ${Hand( - [ - createCard(1), - createCard(2), - createCard(3), - createCard(4), - createCard(4), - createCard(5), - createCard(5), - createCard(5), - ], - 'wolf', - )} - ${Hand([createCard(3), createCard(7), createCard(2), createCard(4)], 'player')} + ${Hand('wolf', $wolfHand, $selectedWolfCardIds)} + ${Hand('player', $playerHand, $selectedPlayerCardIds)} +
+
+ +
` } diff --git a/src/components/card.ts b/src/components/card.ts index 9ce70bf..bfdc5a6 100644 --- a/src/components/card.ts +++ b/src/components/card.ts @@ -5,20 +5,20 @@ import { watch } from '#core/lit-nanostores/watch.ts' import type { GameCard } from '#lobo/card.ts' import { toAtom, toValue, type MaybeAtom } from '#types/atom.ts' -export function Card(card: MaybeAtom, selected: boolean | null = null) { - const value = computed([toAtom(card)], (c) => c.value) - - function click(this: HTMLButtonElement) { - this.dispatchEvent(new CustomEvent('card:click', { bubbles: true, detail: toValue(card) })) - } +export function Card(card: MaybeAtom, selected: MaybeAtom | null = null) { + const $value = computed([toAtom(card)], (c) => c.value) if (selected === null) { return html`
- ${watch(value)} + ${watch($value)}
` } - return html`` } diff --git a/src/components/hand.ts b/src/components/hand.ts index a2bffa6..157669b 100644 --- a/src/components/hand.ts +++ b/src/components/hand.ts @@ -1,29 +1,55 @@ import { html } from 'lit-html' import { repeat } from 'lit-html/directives/repeat.js' -import { computed } from 'nanostores' +import { computed, type ReadableAtom } from 'nanostores' import { watch } from '#core/lit-nanostores/watch.ts' import type { GameCard } from '#lobo/card.ts' -import { toAtom, type MaybeAtom } from '#types/atom.ts' +import type { CardClickEvent } from '#lobo/event.ts' +import type { WolfOrPlayer } from '#lobo/player.ts' +import { toggleSelectPlayerCard, toggleSelectWolfCard } from '#store/actions.ts' +import { toAtom, toValue } from '#types/atom.ts' import { Card } from './card' -export function Hand(cards: MaybeAtom, handOwner: 'player' | 'wolf' = 'wolf') { - const renderedCards = computed( - [toAtom(cards)], - (cardList) => +export function Hand( + handOwner: WolfOrPlayer, + $cards: ReadableAtom, + $selectedIds: ReadableAtom, +) { + const $owner = toAtom(handOwner) + + const $renderedCards = computed( + [$cards, $selectedIds], + (cards, selectedIds) => html`
    ${repeat( - cardList, + cards, (c) => c.id, - (c) => html`
  • ${Card(c, false)}
  • `, + (c) => html`
  • ${Card(c, selectedIds.includes(c.id))}
  • `, )}
`, ) + + function cardClicked(event: CardClickEvent) { + const owner = toValue(handOwner) + const cardId = event.detail.id + + switch (owner) { + case 'wolf': + toggleSelectWolfCard(cardId) + break + case 'player': + toggleSelectPlayerCard(cardId) + break + } + } + return html`
-

${handOwner} cards

-
${watch(renderedCards)}
+

${watch($owner)} cards

+
+ ${watch($renderedCards)} +
` } diff --git a/src/core/library.ts b/src/core/library.ts index ed247cf..2c0ddf3 100644 --- a/src/core/library.ts +++ b/src/core/library.ts @@ -4,10 +4,11 @@ import { shuffle } from '#utils/random.ts' import type { Card } from './card' import { Stack } from './stack' -export class Library { +export class Library extends EventTarget { #stack: Stack> constructor(cards: Card[]) { + super() this.#stack = new Stack(shuffle(cards)) } @@ -25,6 +26,7 @@ export class Library { } if (count > this.#stack.size) { + this.dispatchEvent(new CustomEvent('draw-failed')) return null } @@ -32,6 +34,7 @@ export class Library { for (let i = 1; i < count; i++) { result.push(this.#stack.pop()!) } + this.dispatchEvent(new CustomEvent('draw')) return result } } diff --git a/src/lobo/event.ts b/src/lobo/event.ts new file mode 100644 index 0000000..144ae3d --- /dev/null +++ b/src/lobo/event.ts @@ -0,0 +1,3 @@ +import type { GameCard } from './card' + +export type CardClickEvent = CustomEvent diff --git a/src/lobo/player.ts b/src/lobo/player.ts new file mode 100644 index 0000000..bf12229 --- /dev/null +++ b/src/lobo/player.ts @@ -0,0 +1,3 @@ +export type Wolf = 'wolf' +export type Player = 'player' +export type WolfOrPlayer = Wolf | Player diff --git a/src/store/actions.ts b/src/store/actions.ts index 4b0ce81..2c5ec85 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,4 +1,4 @@ -import type { PreinitializedWritableAtom, ReadableAtom } from 'nanostores' +import type { ReadableAtom, WritableAtom } from 'nanostores' import { getAttackResult } from '#lobo/attack.ts' import { cardSum, type GameCard } from '#lobo/card.ts' @@ -6,7 +6,7 @@ import type { Difficulty } from '#lobo/deck.ts' import { createLoboLibrary } from '#lobo/library.ts' import type { NonEmptyList } from '#types/list.ts' -import { $isValidAttack, $selectedPlayerCards, $selectedWolfCards, $winner } from './derived' +import { $hasGameEnded, $isValidAttack, $selectedPlayerCards, $selectedWolfCards } from './derived' import { $playerHand, $wolfHand, @@ -20,24 +20,17 @@ import { const STARTING_HAND_SIZE = 4 -export function startGame(difficulty: Difficulty = 'ordinary') { - $difficulty.set(difficulty) - $wolfScore.set(0) - $playerScore.set(0) - startRound() -} - /** + * @internal * Shuffling the whole deck per round. * I know that makes the game harder. * I'll think about having multiple rounds between shuffling in the future. * * Also, go drink some water. */ -export function startRound() { - const winner = $winner.get() - if (winner) { - console.debug(`Winner already determined: ${winner}`) +function startRound() { + if ($hasGameEnded.get()) { + console.debug('Game ended') return } @@ -53,10 +46,17 @@ export function startRound() { $playerHand.set(playerHand) } +export function startGame(difficulty: Difficulty = 'ordinary') { + $difficulty.set(difficulty) + $wolfScore.set(0) + $playerScore.set(0) + startRound() +} + /** * @internal */ -function toggleSelectCard($ids: PreinitializedWritableAtom, id: symbol) { +function toggleSelectCard($ids: WritableAtom, id: symbol) { const ids = $ids.get() if (ids.includes(id)) { $ids.set(ids.filter((i) => i !== id)) @@ -74,17 +74,22 @@ export function toggleSelectPlayerCard(id: symbol) { /** * @internal */ -function addCards($hand: PreinitializedWritableAtom, cards: NonEmptyList) { +function addCards($hand: WritableAtom, cards: NonEmptyList) { $hand.set($hand.get().concat(cards)) } /** * @internal */ -function removeCards($hand: PreinitializedWritableAtom, $ids: ReadableAtom) { +function removeCards($hand: WritableAtom, $ids: ReadableAtom) { const ids = $ids.get() - $hand.set($hand.get().filter((gc) => ids.includes(gc.id))) + $hand.set($hand.get().filter((gc) => !ids.includes(gc.id))) } export function attack() { + if ($hasGameEnded.get()) { + console.debug('Game ended') + return + } + const library = $library.get() if (!library || !$isValidAttack.get()) { return @@ -113,6 +118,11 @@ export function attack() { } export function endRound() { + if ($hasGameEnded.get()) { + console.debug('Game ended') + return + } + const wolfHand = $wolfHand.get() const playerHand = $playerHand.get() diff --git a/src/store/derived.ts b/src/store/derived.ts index b08698d..1218af6 100644 --- a/src/store/derived.ts +++ b/src/store/derived.ts @@ -1,6 +1,7 @@ import { computed } from 'nanostores' import { getAttackResult, isValidAttack } from '#lobo/attack.ts' +import type { WolfOrPlayer } from '#lobo/player.ts' import { $library, @@ -34,7 +35,7 @@ export const $isValidAttack = computed( export const $winner = computed( [$wolfScore, $playerScore, $winningScore], - (wolfScore, playerScore, WINNING_SCORE) => { + (wolfScore, playerScore, WINNING_SCORE): WolfOrPlayer | null => { if (wolfScore >= WINNING_SCORE) { return 'wolf' } else if (playerScore >= WINNING_SCORE) { @@ -44,3 +45,5 @@ export const $winner = computed( } }, ) + +export const $hasGameEnded = computed($winner, (winner) => winner !== null) diff --git a/src/types/atom.ts b/src/types/atom.ts index 5f69d25..b7d6576 100644 --- a/src/types/atom.ts +++ b/src/types/atom.ts @@ -2,12 +2,7 @@ import { atom, type Atom } from 'nanostores' export type MaybeAtom = T | Atom export function isAtom(value: MaybeAtom): value is Atom { - return ( - value instanceof Object && - value !== null && - typeof value.lc === 'number' && - typeof value.get === 'function' - ) + return value instanceof Object && value !== null && typeof value.get === 'function' } export function toValue(value: MaybeAtom): T { return isAtom(value) ? value.get() : value