|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- <script src="/libs/mobile-drag-drop.min.js"></script>
-
- const initCards = () => {
- const cards = new Map()
- for(const color of ['red', 'green', 'blue', 'yellow', 'white']) {
- for(const value of [1, 1, 1, 2, 2, 3, 3, 4, 4, 5]) {
- const id = cards.size
- const pos = Hanabi.nrows * Hanabi.ncols - 1
- const ts = Math.random()
- cards.set(id, {id, pos, color, value, ts, faceUp: false})
- }
- }
- return cards
- }
-
- const clip = (v, vmin, vmax) => {
- return Math.min(Math.max(v, vmin), vmax)
- }
-
- const clipToElement = ([x, y], selector) => {
- const boundingArea = document.querySelector(selector)
- const {offsetLeft, offsetTop, offsetWidth, offsetHeight} = boundingArea
- x = clip(x, offsetLeft, offsetLeft + offsetWidth)
- y = clip(y, offsetTop, offsetTop + offsetHeight)
- return [x, y]
- }
-
- const findNearestDropzoneBFS = (cx, cy) => {
- try {
- const dropzone = ['stack', 'card']
- const [x, y] = clipToElement([cx, cy], '.stacks')
- const deque = [JSON.stringify([x, y])]
- const seen = new Set(deque)
- while(deque.length && deque.length < 50) {
- const coord = deque.shift()
- const [x, y] = JSON.parse(coord)
- for(const dx of [-1, 0, 1]) {
- for(const dy of [-1, 0, 1]) {
- const [nx, ny] = [x + dx, y + dy]
- const ncoord = JSON.stringify([nx, ny])
- if(!seen.has(ncoord)) {
- deque.push(ncoord)
- seen.add(ncoord)
- }
- }
- }
- target = document.elementFromPoint(x - scrollX, y - scrollY)
- isValid = target && dropzone.filter(s => target.classList.contains(s)).length
- if(isValid) {
- return target
- }
- }
- throw 'dropzone not found'
- }
- catch(error) {
- Chat.log(error)
- const stacks = [...document.querySelectorAll('.stack')]
- return stacks[Hanabi.nrows * Hanabi.ncols - 1]
- }
- }
-
- const Counter = {
- step: (name, delta) => () => {
- Hanabi.counters[name] += delta
- Hanabi.sync()
- },
- view: ({attrs}) => m('.counter',
- m('span', attrs.name),
- m('button', {onclick: Counter.step(attrs.name, -1)}, '-'),
- m('span', Hanabi.counters[attrs.name]),
- m('button', {onclick: Counter.step(attrs.name, +1)}, '+'),
- ),
- }
-
- const Hanabi = {
- nrows: 8,
- ncols: 7,
- counters: {clues: 8, bombs: 3},
- players: new Map(),
- cards: [],
- oninit: () => {
- Hanabi.cards = initCards()
- listen('hanabi', ({detail}) => {
- Hanabi.counters = detail.value.counters
- Hanabi.cards = new Map(detail.value.cards)
- })
- listen('join', () => setTimeout(Hanabi.sync, 100))
- doNotLog.add('hanabi')
- },
- sync: () => {
- wire({kind: 'hanabi', value: {
- counters: Hanabi.counters,
- cards: [...Hanabi.cards.entries()],
- }})
- },
- getStacks: () => {
- const totalCount = Hanabi.nrows * Hanabi.ncols
- const stacks = new Array(totalCount).fill(null).map(_ => [])
- Hanabi.cards.forEach((card) => stacks[card.pos].push(card))
- return stacks
- },
- renderCard: (card, i) => {
- const attrs = {
- id: card.id,
- value: card.value,
- style: {top: `${-3 * i}px`},
- ondragstart: (ev) => {
- ev.dataTransfer.setData('idx', card.id)
- ev.dataTransfer.dropEffect = 'move'
- },
- ondragend: (ev) => {
- for(const el of document.querySelectorAll('.drag-target')) {
- el.classList.remove('drag-target')
- }
- },
- class: ['card', card.color, card.faceUp && 'face-up'].join(' '),
- draggable: true,
- }
- return m('div', attrs)
- },
- renderStack: (pos, stacks) => {
- const stack = stacks[pos]
- stack.sort((a, b) => a.ts - b.ts)
- const attrs = {
- pos: pos,
- ondrop: (ev) => {
- ev.preventDefault()
- const card = Hanabi.cards.get(+ev.dataTransfer.getData('idx'))
- card.pos = pos
- card.ts = +new Date()
- Hanabi.sync()
- },
- ondragenter: (ev) => {
- /* needed for drag-drop shim */
- ev.preventDefault()
- for(const el of document.querySelectorAll('.drag-target')) {
- el.classList.remove('drag-target')
- }
- ev.target.classList.add('drag-target')
- },
- ondragover: (ev) => {
- ev.preventDefault()
- ev.dataTransfer.dropEffect = 'move'
- },
- }
- const doMany = (fn) => {
- return {onclick: () => {stack.forEach(fn); Hanabi.sync()}}
- }
- const actions = [
- m('button', doMany(card => card.faceUp = !card.faceUp), 'flip'),
- m('button', doMany(card => card.ts = card.id), 'sort'),
- m('button', doMany(card => card.ts = Math.random()), 'shuffle'),
- m('button', doMany(card => card.faceUp = true), 'reveal'),
- ]
- return m('.stack', attrs, stack.map(Hanabi.renderCard),
- stack.length ? m('.actions', actions) : null,
- )
- },
- renderStacks: () => {
- const stacks = Hanabi.getStacks()
- return m('.stacks', {style: {'--ncols': Hanabi.ncols}},
- Array(Hanabi.nrows).fill(null).map((_, y) =>
- Array(Hanabi.ncols).fill(null).map((_, x) =>
- m('.col',
- {owner: State.online[y], class: y < 5 ? State.username === State.online[y] ? 'own' : 'rival' : ''},
- Hanabi.renderStack(y * Hanabi.ncols + x, stacks)),
- )
- )
- )
- },
- view: () => m('.hanabi',
- Hanabi.renderStacks(),
- m(Counter, {name: 'clues'}),
- m(Counter, {name: 'bombs'}),
- ),
- }
-
- // requires mobile-drag-drop.js
- MobileDragDrop.polyfill({
- elementFromPoint: findNearestDropzoneBFS,
- })
- addEventListener('touchmove', () => {})
-
|