You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 line
6.1KB

  1. <script src="/libs/mobile-drag-drop.min.js"></script>
  2. const initCards = () => {
  3. const cards = new Map()
  4. for(const color of ['red', 'green', 'blue', 'yellow', 'white']) {
  5. for(const value of [1, 1, 1, 2, 2, 3, 3, 4, 4, 5]) {
  6. const id = cards.size
  7. const pos = Hanabi.nrows * Hanabi.ncols - 1
  8. const ts = Math.random()
  9. cards.set(id, {id, pos, color, value, ts, faceUp: false})
  10. }
  11. }
  12. return cards
  13. }
  14. const clip = (v, vmin, vmax) => {
  15. return Math.min(Math.max(v, vmin), vmax)
  16. }
  17. const clipToElement = ([x, y], selector) => {
  18. const boundingArea = document.querySelector(selector)
  19. const {offsetLeft, offsetTop, offsetWidth, offsetHeight} = boundingArea
  20. x = clip(x, offsetLeft, offsetLeft + offsetWidth)
  21. y = clip(y, offsetTop, offsetTop + offsetHeight)
  22. return [x, y]
  23. }
  24. const findNearestDropzoneBFS = (cx, cy) => {
  25. try {
  26. const dropzone = ['stack', 'card']
  27. const [x, y] = clipToElement([cx, cy], '.stacks')
  28. const deque = [JSON.stringify([x, y])]
  29. const seen = new Set(deque)
  30. while(deque.length && deque.length < 50) {
  31. const coord = deque.shift()
  32. const [x, y] = JSON.parse(coord)
  33. for(const dx of [-1, 0, 1]) {
  34. for(const dy of [-1, 0, 1]) {
  35. const [nx, ny] = [x + dx, y + dy]
  36. const ncoord = JSON.stringify([nx, ny])
  37. if(!seen.has(ncoord)) {
  38. deque.push(ncoord)
  39. seen.add(ncoord)
  40. }
  41. }
  42. }
  43. target = document.elementFromPoint(x - scrollX, y - scrollY)
  44. isValid = target && dropzone.filter(s => target.classList.contains(s)).length
  45. if(isValid) {
  46. return target
  47. }
  48. }
  49. throw 'dropzone not found'
  50. }
  51. catch(error) {
  52. Chat.log(error)
  53. const stacks = [...document.querySelectorAll('.stack')]
  54. return stacks[Hanabi.nrows * Hanabi.ncols - 1]
  55. }
  56. }
  57. const Counter = {
  58. step: (name, delta) => () => {
  59. Hanabi.counters[name] += delta
  60. Hanabi.sync()
  61. },
  62. view: ({attrs}) => m('.counter',
  63. m('span', attrs.name),
  64. m('button', {onclick: Counter.step(attrs.name, -1)}, '-'),
  65. m('span', Hanabi.counters[attrs.name]),
  66. m('button', {onclick: Counter.step(attrs.name, +1)}, '+'),
  67. ),
  68. }
  69. const Hanabi = {
  70. nrows: 8,
  71. ncols: 7,
  72. counters: {clues: 8, bombs: 3},
  73. players: new Map(),
  74. cards: [],
  75. oninit: () => {
  76. Hanabi.cards = initCards()
  77. listen('hanabi', ({detail}) => {
  78. Hanabi.counters = detail.value.counters
  79. Hanabi.cards = new Map(detail.value.cards)
  80. })
  81. listen('join', () => setTimeout(Hanabi.sync, 100))
  82. doNotLog.add('hanabi')
  83. },
  84. sync: () => {
  85. wire({kind: 'hanabi', value: {
  86. counters: Hanabi.counters,
  87. cards: [...Hanabi.cards.entries()],
  88. }})
  89. },
  90. getStacks: () => {
  91. const totalCount = Hanabi.nrows * Hanabi.ncols
  92. const stacks = new Array(totalCount).fill(null).map(_ => [])
  93. Hanabi.cards.forEach((card) => stacks[card.pos].push(card))
  94. return stacks
  95. },
  96. renderCard: (card, i) => {
  97. const attrs = {
  98. id: card.id,
  99. value: card.value,
  100. style: {top: `${-3 * i}px`},
  101. ondragstart: (ev) => {
  102. ev.dataTransfer.setData('idx', card.id)
  103. ev.dataTransfer.dropEffect = 'move'
  104. },
  105. ondragend: (ev) => {
  106. for(const el of document.querySelectorAll('.drag-target')) {
  107. el.classList.remove('drag-target')
  108. }
  109. },
  110. class: ['card', card.color, card.faceUp && 'face-up'].join(' '),
  111. draggable: true,
  112. }
  113. return m('div', attrs)
  114. },
  115. renderStack: (pos, stacks) => {
  116. const stack = stacks[pos]
  117. stack.sort((a, b) => a.ts - b.ts)
  118. const attrs = {
  119. pos: pos,
  120. ondrop: (ev) => {
  121. ev.preventDefault()
  122. const card = Hanabi.cards.get(+ev.dataTransfer.getData('idx'))
  123. card.pos = pos
  124. card.ts = +new Date()
  125. Hanabi.sync()
  126. },
  127. ondragenter: (ev) => {
  128. /* needed for drag-drop shim */
  129. ev.preventDefault()
  130. for(const el of document.querySelectorAll('.drag-target')) {
  131. el.classList.remove('drag-target')
  132. }
  133. ev.target.classList.add('drag-target')
  134. },
  135. ondragover: (ev) => {
  136. ev.preventDefault()
  137. ev.dataTransfer.dropEffect = 'move'
  138. },
  139. }
  140. const doMany = (fn) => {
  141. return {onclick: () => {stack.forEach(fn); Hanabi.sync()}}
  142. }
  143. const actions = [
  144. m('button', doMany(card => card.faceUp = !card.faceUp), 'flip'),
  145. m('button', doMany(card => card.ts = card.id), 'sort'),
  146. m('button', doMany(card => card.ts = Math.random()), 'shuffle'),
  147. m('button', doMany(card => card.faceUp = true), 'reveal'),
  148. ]
  149. return m('.stack', attrs, stack.map(Hanabi.renderCard),
  150. stack.length ? m('.actions', actions) : null,
  151. )
  152. },
  153. renderStacks: () => {
  154. const stacks = Hanabi.getStacks()
  155. return m('.stacks', {style: {'--ncols': Hanabi.ncols}},
  156. Array(Hanabi.nrows).fill(null).map((_, y) =>
  157. Array(Hanabi.ncols).fill(null).map((_, x) =>
  158. m('.col',
  159. {owner: State.online[y], class: y < 5 ? State.username === State.online[y] ? 'own' : 'rival' : ''},
  160. Hanabi.renderStack(y * Hanabi.ncols + x, stacks)),
  161. )
  162. )
  163. )
  164. },
  165. view: () => m('.hanabi',
  166. Hanabi.renderStacks(),
  167. m(Counter, {name: 'clues'}),
  168. m(Counter, {name: 'bombs'}),
  169. ),
  170. }
  171. // requires mobile-drag-drop.js
  172. MobileDragDrop.polyfill({
  173. elementFromPoint: findNearestDropzoneBFS,
  174. })
  175. addEventListener('touchmove', () => {})