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.

182 line
6.0KB

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