Skip to content

Commit

Permalink
Merge pull request #119 from emiljohansson/feature/spider-undo-states
Browse files Browse the repository at this point in the history
Feature/spider undo states
  • Loading branch information
emiljohansson authored Jan 15, 2024
2 parents 35d64cc + f77933a commit 7fc7ea7
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 91 deletions.
1 change: 1 addition & 0 deletions apps/games/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"framer-motion": "6.2.3",
"just-shuffle": "4.1.1",
"lib": "workspace:*",
"lz-string": "1.5.0",
"million": "2.6.4",
"nanostores": "0.9.5",
"next": "14.0.4",
Expand Down
34 changes: 16 additions & 18 deletions apps/games/src/app/idiot/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
deselectAll,
moveCardsToPiles,
removeEmptyLeadingCards,
scaleGame,
} from 'src/lib/game'
import { usePreloadCards } from 'src/lib/hooks'

Expand Down Expand Up @@ -55,7 +54,6 @@ export function Game({
const [newPiles, newDeck] = moveCardsToPiles(updatedPiles, deck)
setPiles([...newPiles])
setDeck([...newDeck])
setTimeout(() => scaleGame(mainRef.current))
}

function handleSelectedCard(current: Card, index: number) {
Expand Down Expand Up @@ -117,10 +115,25 @@ export function Game({
<>
<Header>
<HeaderAction onClick={() => location.reload()} data-test="refresh">
<FiRefreshCw width={30} height={30} />
<FiRefreshCw size={30} />
<span className="sr-only">New Game</span>
</HeaderAction>
</Header>
<nav className="h-16">
<button className="h-full w-20 ml-4 relative" onClick={addMoreCards}>
{chunk(deck, 4).map((card, index) => (
<img
key={index}
className="h-full py-1 absolute top-0"
style={{
left: 4 * index,
}}
src="/images/cards/red_back.png"
alt="add more cards"
/>
))}
</button>
</nav>
<main ref={mainRef} className="mx-auto p-4 max-w-screen-lg">
<h1 className="sr-only">The Idiot Card Game</h1>
<div className="flex">
Expand Down Expand Up @@ -151,21 +164,6 @@ export function Game({
))}
</div>
</main>
<footer className="h-16 relative">
<button className="h-full w-20 ml-4 relative" onClick={addMoreCards}>
{chunk(deck, 4).map((card, index) => (
<img
key={index}
className="h-full py-1 absolute top-0"
style={{
left: 4 * index,
}}
src="/images/cards/red_back.png"
alt="add more cards"
/>
))}
</button>
</footer>
</>
)
}
95 changes: 52 additions & 43 deletions apps/games/src/app/spider/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import type { Card, Deck, Piles } from 'src/types/card-games'
import { useRef, useState } from 'react'
import { FiRefreshCw, FiRotateCcw } from 'react-icons/fi'
import { isDefined, isEmpty } from 'lib/utils/lang'
import { classNames } from 'lib/utils/string'
import { chunk, first, last } from 'lib/utils/array'
import { Header, HeaderAction } from 'ui'
import {
deselectAll,
moveCardsToPiles,
removeEmptyLeadingCards,
scaleGame,
} from 'src/lib/game'
import { usePreloadCards } from 'src/lib/hooks'
import { createBaseDeck } from './createBaseDeck'
import { useRouter } from 'next/navigation'
import { Image } from './Image'
import { restoreGameFromHash, saveGameToHash } from './state'

const getClickableIndexesFromPile = (pile: Card[]) => {
if (pile.length < 1) return []
Expand All @@ -32,45 +33,29 @@ const getClickableIndexesFromPile = (pile: Card[]) => {
return result
}

const Image = ({ card, cardImage }: { card: Card; cardImage: string }) => (
<img
src={`/images/cards/${cardImage}.png`}
alt={card?.combined ?? 'blank card'}
className={classNames(
`
border-4 border-transparent border-solid rounded-lg
relative top-0 left-0
mx-auto
w-[calc(100%-0px)]
`,
{
'bg-primary': card?.selected,
},
)}
/>
)

export function Game({
remainingCards,
initPiles,
}: {
remainingCards: Deck
initPiles: Piles
}) {
const router = useRouter()
const [deck, setDeck] = useState<Deck>(remainingCards)
const [piles, setPiles] = useState<Piles>(initPiles)
// const [prevMove, setPrevMove] = useState<number[]>([])
const mainRef = useRef<HTMLElement>(null)

usePreloadCards(createBaseDeck())

function addMoreCards() {
deselectAll(piles)
initHashState()
if (isEmpty(deck)) return
const updatedPiles = removeEmptyLeadingCards(piles)
const [newPiles, newDeck] = moveCardsToPiles(updatedPiles, deck)
setPiles([...newPiles])
setDeck([...newDeck])
setTimeout(() => scaleGame(mainRef.current))
saveGameToHash(newDeck, newPiles)
}

function getSelectedCard() {
Expand All @@ -86,6 +71,7 @@ export function Game({

function handleSelectedCard(current: Card, currentPileIndex: number) {
console.log(current)
initHashState()

const [selectedCard, selectedPileIndex, selectedCardIndex] =
getSelectedCard()
Expand Down Expand Up @@ -167,22 +153,60 @@ export function Game({
}
}
setPiles(newPiles)
setTimeout(() => scaleGame(mainRef.current))
saveGameToHash(deck, piles)
}

function undoMove() {
if (window.location.hash === '') return
router.back()
setTimeout(() => {
const restored = restoreGameFromHash()
if (!isDefined(restored)) return
setDeck(restored.deck)
setPiles(restored.piles)
})
}

function initHashState() {
if (window.location.hash !== '') return
console.log('init game')
saveGameToHash(deck, piles)
}

return (
<>
<Header>
<HeaderAction onClick={() => location.reload()} data-test="new-game">
<HeaderAction
onClick={() => {
window.location.hash = ''
location.reload()
}}
data-test="new-game"
>
<FiRefreshCw size={30} strokeWidth="1.5" />
<span className="sr-only">New Game</span>
New Game
</HeaderAction>
<HeaderAction onClick={() => console.log('undo')} data-test="undo">
<HeaderAction onClick={undoMove} data-test="undo">
<FiRotateCcw size={30} strokeWidth="1.5" />
<span className="sr-only">Undo</span>
Undo
</HeaderAction>
</Header>
<main ref={mainRef} className="mx-auto p-4 max-w-screen-lg">
<nav className="h-16">
<button className="h-full w-20 ml-4 relative" onClick={addMoreCards}>
{chunk(deck, 10).map((card, index) => (
<img
key={index}
className="h-full py-1 absolute top-0"
style={{
left: 4 * index,
}}
src="/images/cards/red_back.png"
alt="add more cards"
/>
))}
</button>
</nav>
<main ref={mainRef} className="mx-auto px-4 max-w-screen-lg">
<h1 className="sr-only">Spider Solitaire</h1>
<div className="flex">
{piles.map((pile, pileIndex) => (
Expand Down Expand Up @@ -218,21 +242,6 @@ export function Game({
))}
</div>
</main>
<footer className="h-16 relative">
<button className="h-full w-20 ml-4 relative" onClick={addMoreCards}>
{chunk(deck, 10).map((card, index) => (
<img
key={index}
className="h-full py-1 absolute top-0"
style={{
left: 4 * index,
}}
src="/images/cards/red_back.png"
alt="add more cards"
/>
))}
</button>
</footer>
</>
)
}
26 changes: 26 additions & 0 deletions apps/games/src/app/spider/Image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Card } from 'src/types/card-games'
import { classNames } from 'lib/utils/string'

export const Image = ({
card,
cardImage,
}: {
card: Card
cardImage: string
}) => (
<img
src={`/images/cards/${cardImage}.png`}
alt={card?.combined ?? 'blank card'}
className={classNames(
`
border-4 border-transparent border-solid rounded-lg
relative top-0 left-0
mx-auto
w-[calc(100%-0px)]
`,
{
'bg-primary': card?.selected,
},
)}
/>
)
85 changes: 56 additions & 29 deletions apps/games/src/app/spider/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,66 @@
'use client'

import shuffle from 'just-shuffle'
import { Game } from './Game'
import { createBaseDeck } from './createBaseDeck'
import { useEffect, useState } from 'react'
import { isDefined } from 'lib/utils/lang'
import { Deck, Piles } from '@/types/card-games'
import { restoreGameFromHash } from './state'

export const metadata = {
title: 'Spider Solitaire',
description: 'Spider Solitaire',
}
// export const metadata = {
// title: 'Spider Solitaire',
// description: 'Spider Solitaire',
// }

export default function Page() {
const deck = shuffle(
[
createBaseDeck(),
createBaseDeck(),
createBaseDeck(),
createBaseDeck(),
].flat(),
)
const [deck, setDeck] = useState<Deck>()
const [piles, setPiles] = useState<Piles>()

const initPiles = [
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
]
initPiles.forEach((pile) => {
pile.slice(0, pile.length - 1).forEach((card) => {
card.hidden = true
useEffect(() => {
const restored = restoreGameFromHash()
if (isDefined(restored)) {
setDeck(restored.deck)
setPiles(restored.piles)
return
}
const deck = shuffle(
[
createBaseDeck(),
createBaseDeck(),
createBaseDeck(),
createBaseDeck(),
].flat(),
)

const initPiles = [
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 6),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
deck.splice(0, 5),
]
initPiles.forEach((pile) => {
pile.slice(0, pile.length - 1).forEach((card) => {
card.hidden = true
})
})
})
setDeck(deck)
setPiles(initPiles)
}, [])

return <Game remainingCards={deck} initPiles={initPiles} />
return (
<>
{!deck || !piles ? (
<></>
) : (
<Game remainingCards={deck} initPiles={piles} />
)}
</>
)
}
24 changes: 24 additions & 0 deletions apps/games/src/app/spider/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Deck, Piles } from '@/types/card-games'
import {
compress,
compressToBase64,
decompressFromBase64,
decompress,
} from 'lz-string'

export function saveGameToHash(deck: Deck, piles: Piles) {
const data = JSON.stringify({ deck, piles })
const compressed = compress(data)
const encoded = compressToBase64(compressed)
window.location.hash = encoded
}

export function restoreGameFromHash() {
const hash = window.location.hash
if (hash === '') return
const encoded = hash.substring(1)
const decoded = decompressFromBase64(encoded)
const decompressed = decompress(decoded)
const { deck, piles } = JSON.parse(decompressed)
return { deck, piles }
}
2 changes: 1 addition & 1 deletion apps/games/src/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const usePreloadCards = (deck: Deck) => {
deck.forEach((card) => {
const img = new Image()
img.onload = () => {
console.log('loaded', card.combined)
// console.log('loaded', card.combined)
}
img.src = `/images/cards/${card?.combined}.png`
})
Expand Down
Loading

2 comments on commit 7fc7ea7

@vercel
Copy link

@vercel vercel bot commented on 7fc7ea7 Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 7fc7ea7 Jan 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

emiljohansson.dev – ./apps/next

emiljohanssondev-emiljohansson.vercel.app
emiljohanssondev-git-main-emiljohansson.vercel.app
emiljohansson.dev

Please sign in to comment.