guandan.dev

guandan.dev

https://git.tonybtw.com/guandan.dev.git git://git.tonybtw.com/guandan.dev.git
7,769 bytes raw
1
import { useCallback, useEffect, useState } from 'react'
2
import { use_websocket } from './hooks/use_websocket'
3
import { Lobby } from './components/Lobby'
4
import { Game } from './components/Game'
5
import {
6
  Card,
7
  Player_Info,
8
  Rank,
9
  Rank_Two,
10
  Message,
11
} from './game/types'
12
13
interface Deal_Cards_Payload {
14
  cards: Card[]
15
  level: Rank
16
}
17
18
interface Room_State_Payload {
19
  room_id: string
20
  players: Player_Info[]
21
  game_active: boolean
22
  your_id: string
23
}
24
25
interface Turn_Payload {
26
  player_id: string
27
  seat: number
28
  can_pass: boolean
29
}
30
31
interface Play_Made_Payload {
32
  player_id: string
33
  seat: number
34
  cards: Card[]
35
  combo_type: string
36
  is_pass: boolean
37
}
38
39
interface Error_Payload {
40
  message: string
41
}
42
43
export default function App() {
44
  const ws_url = `ws://${window.location.hostname}:8080/ws`
45
  const { connected, send, on } = use_websocket(ws_url)
46
47
  const [room_id, set_room_id] = useState<string | null>(null)
48
  const [players, set_players] = useState<Player_Info[]>([])
49
  const [game_active, set_game_active] = useState(false)
50
51
  const [hand, set_hand] = useState<Card[]>([])
52
  const [level, set_level] = useState<Rank>(Rank_Two)
53
  const [selected_ids, set_selected_ids] = useState<Set<number>>(new Set())
54
  const [current_turn, set_current_turn] = useState(0)
55
  const [my_seat, set_my_seat] = useState(0)
56
  const [can_pass, set_can_pass] = useState(false)
57
  const [table_cards, set_table_cards] = useState<Card[]>([])
58
  const [combo_type, set_combo_type] = useState('')
59
  const [player_card_counts, set_player_card_counts] = useState([27, 27, 27, 27])
60
  const [team_levels, set_team_levels] = useState<[number, number]>([0, 0])
61
  const [error, set_error] = useState<string | null>(null)
62
  const [play_log, set_play_log] = useState<Array<{ seat: number; cards: Card[]; combo_type: string; is_pass: boolean }>>([])
63
  const [players_map, set_players_map] = useState<Record<number, string>>({})
64
  const [last_play_seat, set_last_play_seat] = useState<number | null>(null)
65
66
  useEffect(() => {
67
    const unsub_room_state = on('room_state', (msg: Message) => {
68
      const payload = msg.payload as Room_State_Payload
69
      set_room_id(payload.room_id)
70
      set_players(payload.players)
71
      set_game_active(payload.game_active)
72
73
      const me = payload.players.find((p) => p.id === payload.your_id)
74
      if (me) {
75
        set_my_seat(me.seat)
76
      }
77
      const pmap: Record<number, string> = {}
78
      payload.players.forEach((p) => {
79
        pmap[p.seat] = p.name
80
      })
81
      set_players_map(pmap)
82
    })
83
84
    const unsub_deal = on('deal_cards', (msg: Message) => {
85
      const payload = msg.payload as Deal_Cards_Payload
86
      set_hand(sort_cards(payload.cards, payload.level))
87
      set_level(payload.level)
88
      set_game_active(true)
89
      set_table_cards([])
90
      set_combo_type('')
91
      set_selected_ids(new Set())
92
      set_player_card_counts([27, 27, 27, 27])
93
      set_play_log([])
94
    })
95
96
    const unsub_turn = on('turn', (msg: Message) => {
97
      const payload = msg.payload as Turn_Payload
98
      set_current_turn(payload.seat)
99
      set_can_pass(payload.can_pass)
100
    })
101
102
    const unsub_play_made = on('play_made', (msg: Message) => {
103
      const payload = msg.payload as Play_Made_Payload
104
105
      set_play_log((prev) => {
106
        const next = [...prev, {
107
          seat: payload.seat,
108
          cards: payload.cards || [],
109
          combo_type: payload.combo_type || '',
110
          is_pass: payload.is_pass,
111
        }]
112
        return next.slice(-8)
113
      })
114
115
      set_last_play_seat(payload.seat)
116
      setTimeout(() => set_last_play_seat(null), 800)
117
118
      if (!payload.is_pass) {
119
        set_table_cards(payload.cards)
120
        set_combo_type(payload.combo_type)
121
        set_player_card_counts((prev) => {
122
          const next = [...prev]
123
          next[payload.seat] -= payload.cards.length
124
          return next as [number, number, number, number]
125
        })
126
        const played_ids = new Set(payload.cards.map((c) => c.Id))
127
        set_hand((prev) => prev.filter((c) => !played_ids.has(c.Id)))
128
      }
129
    })
130
131
    const unsub_hand_end = on('hand_end', (msg: Message) => {
132
      const payload = msg.payload as { new_levels: [number, number] }
133
      set_team_levels(payload.new_levels)
134
    })
135
136
    const unsub_error = on('error', (msg: Message) => {
137
      const payload = msg.payload as Error_Payload
138
      set_error(payload.message)
139
      setTimeout(() => set_error(null), 3000)
140
    })
141
142
    return () => {
143
      unsub_room_state()
144
      unsub_deal()
145
      unsub_turn()
146
      unsub_play_made()
147
      unsub_hand_end()
148
      unsub_error()
149
    }
150
  }, [on])
151
152
  const handle_create_room = useCallback(
153
    (name: string) => {
154
      send({
155
        type: 'create_room',
156
        payload: { player_name: name },
157
      })
158
    },
159
    [send]
160
  )
161
162
  const handle_join_room = useCallback(
163
    (room_code: string, name: string) => {
164
      send({
165
        type: 'join_room',
166
        payload: { room_id: room_code, player_name: name },
167
      })
168
    },
169
    [send]
170
  )
171
172
  const handle_fill_bots = useCallback(() => {
173
    send({ type: 'fill_bots', payload: {} })
174
  }, [send])
175
176
  const handle_card_click = useCallback((id: number) => {
177
    set_selected_ids((prev) => {
178
      const next = new Set(prev)
179
      if (next.has(id)) {
180
        next.delete(id)
181
      } else {
182
        next.add(id)
183
      }
184
      return next
185
    })
186
  }, [])
187
188
  const handle_play = useCallback(() => {
189
    if (selected_ids.size === 0) return
190
191
    send({
192
      type: 'play_cards',
193
      payload: { card_ids: Array.from(selected_ids) },
194
    })
195
196
    set_selected_ids(new Set())
197
  }, [send, selected_ids])
198
199
  const handle_pass = useCallback(() => {
200
    send({ type: 'pass', payload: {} })
201
  }, [send])
202
203
  if (!connected) {
204
    return (
205
      <div style={styles.connecting}>
206
        <div>Connecting...</div>
207
      </div>
208
    )
209
  }
210
211
  if (!game_active) {
212
    return (
213
      <>
214
        <Lobby
215
          room_id={room_id}
216
          players={players}
217
          on_create_room={handle_create_room}
218
          on_join_room={handle_join_room}
219
          on_fill_bots={handle_fill_bots}
220
        />
221
        {error && <div style={styles.error}>{error}</div>}
222
      </>
223
    )
224
  }
225
226
  return (
227
    <>
228
      <Game
229
        hand={hand}
230
        level={level}
231
        selected_ids={selected_ids}
232
        on_card_click={handle_card_click}
233
        on_play={handle_play}
234
        on_pass={handle_pass}
235
        table_cards={table_cards}
236
        combo_type={combo_type}
237
        current_turn={current_turn}
238
        my_seat={my_seat}
239
        can_pass={can_pass}
240
        player_card_counts={player_card_counts}
241
        team_levels={team_levels}
242
        play_log={play_log}
243
        players_map={players_map}
244
        last_play_seat={last_play_seat}
245
      />
246
      {error && <div style={styles.error}>{error}</div>}
247
    </>
248
  )
249
}
250
251
function sort_cards(cards: Card[], level: Rank): Card[] {
252
  return [...cards].sort((a, b) => {
253
    const va = card_sort_value(a, level)
254
    const vb = card_sort_value(b, level)
255
    if (va !== vb) return va - vb
256
    return a.Suit - b.Suit
257
  })
258
}
259
260
function card_sort_value(card: Card, level: Rank): number {
261
  if (card.Rank === 14) return 100
262
  if (card.Rank === 13) return 99
263
  if (card.Rank === level) return 98
264
265
  const base_order = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
266
  return base_order[card.Rank] ?? 0
267
}
268
269
const styles: Record<string, React.CSSProperties> = {
270
  connecting: {
271
    display: 'flex',
272
    justifyContent: 'center',
273
    alignItems: 'center',
274
    height: '100vh',
275
    backgroundColor: '#1a1a2e',
276
    color: '#fff',
277
    fontSize: 24,
278
  },
279
  error: {
280
    position: 'fixed',
281
    bottom: 20,
282
    left: '50%',
283
    transform: 'translateX(-50%)',
284
    padding: '12px 24px',
285
    backgroundColor: '#dc3545',
286
    color: '#fff',
287
    borderRadius: 8,
288
    zIndex: 1000,
289
  },
290
}