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', () => {})