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.

115 lines
3.8KB

  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 Counter = {
  14. step: (name, delta) => () => {
  15. Hanabi.counters[name] += delta
  16. Hanabi.sync()
  17. },
  18. view: ({attrs}) => m('.counter',
  19. m('span', attrs.name),
  20. m('button', {onclick: Counter.step(attrs.name, -1)}, '-'),
  21. m('span', Hanabi.counters[attrs.name]),
  22. m('button', {onclick: Counter.step(attrs.name, +1)}, '+'),
  23. ),
  24. }
  25. const Hanabi = {
  26. nrows: 8,
  27. ncols: 7,
  28. counters: {clues: 8, bombs: 3},
  29. players: new Map(),
  30. cards: [],
  31. oninit: () => {
  32. Hanabi.cards = initCards()
  33. listen('hanabi', ({detail}) => {
  34. Hanabi.counters = detail.value.counters
  35. Hanabi.cards = new Map(detail.value.cards)
  36. })
  37. listen('join', () => setTimeout(Hanabi.sync, 100))
  38. doNotLog.add('hanabi')
  39. },
  40. sync: () => {
  41. wire({kind: 'hanabi', value: {
  42. counters: Hanabi.counters,
  43. cards: [...Hanabi.cards.entries()],
  44. }})
  45. },
  46. getStacks: () => {
  47. const totalCount = Hanabi.nrows * Hanabi.ncols
  48. const stacks = new Array(totalCount).fill(null).map(_ => [])
  49. Hanabi.cards.forEach((card) => stacks[card.pos].push(card))
  50. return stacks
  51. },
  52. renderCard: (card, i) => {
  53. const attrs = {
  54. id: card.id,
  55. value: card.value,
  56. style: {top: `${-3 * i}px`},
  57. ondragstart: (ev) => {
  58. ev.dataTransfer.setData('idx', card.id)
  59. ev.dataTransfer.dropEffect = 'move'
  60. },
  61. class: ['card', card.color, card.faceUp && 'face-up'].join(' '),
  62. draggable: true,
  63. }
  64. return m('div', attrs)
  65. },
  66. renderStack: (pos, stacks) => {
  67. const stack = stacks[pos]
  68. stack.sort((a, b) => a.ts - b.ts)
  69. const attrs = {
  70. ondrop: (ev) => {
  71. ev.preventDefault()
  72. const card = Hanabi.cards.get(+ev.dataTransfer.getData('idx'))
  73. card.pos = pos
  74. card.ts = +new Date()
  75. Hanabi.sync()
  76. },
  77. ondragover: (ev) => {
  78. ev.preventDefault()
  79. ev.dataTransfer.dropEffect = 'move'
  80. },
  81. }
  82. const doMany = (fn) => {
  83. return {onclick: () => {stack.forEach(fn); Hanabi.sync()}}
  84. }
  85. const actions = [
  86. m('button', doMany(card => card.faceUp = !card.faceUp), 'flip'),
  87. m('button', doMany(card => card.ts = card.id), 'sort'),
  88. m('button', doMany(card => card.ts = Math.random()), 'shuffle'),
  89. m('button', doMany(card => card.faceUp = true), 'reveal'),
  90. ]
  91. return m('.stack', attrs, stack.map(Hanabi.renderCard),
  92. stack.length ? m('.actions', actions) : null,
  93. )
  94. },
  95. renderStacks: () => {
  96. const stacks = Hanabi.getStacks()
  97. return m('.stacks', {style: {'--ncols': Hanabi.ncols}},
  98. Array(Hanabi.nrows).fill(null).map((_, y) =>
  99. Array(Hanabi.ncols).fill(null).map((_, x) =>
  100. m('.col',
  101. {owner: State.online[y], class: y < 5 ? State.username === State.online[y] ? 'own' : 'rival' : ''},
  102. Hanabi.renderStack(y * Hanabi.ncols + x, stacks)),
  103. )
  104. )
  105. )
  106. },
  107. view: () => m('.hanabi',
  108. m(Counter, {name: 'clues'}),
  109. m(Counter, {name: 'bombs'}),
  110. Hanabi.renderStacks(),
  111. ),
  112. }