import { useCallback, useEffect, useState } from 'react' import { use_websocket } from './hooks/use_websocket' import { Lobby } from './components/Lobby' import { Game } from './components/Game' import { Card, Player_Info, Rank, Rank_Two, Message, } from './game/types' interface Deal_Cards_Payload { cards: Card[] level: Rank } interface Room_State_Payload { room_id: string players: Player_Info[] game_active: boolean your_id: string } interface Turn_Payload { player_id: string seat: number can_pass: boolean } interface Play_Made_Payload { player_id: string seat: number cards: Card[] combo_type: string is_pass: boolean } interface Error_Payload { message: string } export default function App() { const ws_url = `ws://${window.location.hostname}:8080/ws` const { connected, send, on } = use_websocket(ws_url) const [room_id, set_room_id] = useState(null) const [players, set_players] = useState([]) const [game_active, set_game_active] = useState(false) const [hand, set_hand] = useState([]) const [level, set_level] = useState(Rank_Two) const [selected_ids, set_selected_ids] = useState>(new Set()) const [current_turn, set_current_turn] = useState(0) const [my_seat, set_my_seat] = useState(0) const [can_pass, set_can_pass] = useState(false) const [table_cards, set_table_cards] = useState([]) const [combo_type, set_combo_type] = useState('') const [player_card_counts, set_player_card_counts] = useState([27, 27, 27, 27]) const [team_levels, set_team_levels] = useState<[number, number]>([0, 0]) const [error, set_error] = useState(null) const [play_log, set_play_log] = useState>([]) const [players_map, set_players_map] = useState>({}) const [last_play_seat, set_last_play_seat] = useState(null) useEffect(() => { const unsub_room_state = on('room_state', (msg: Message) => { const payload = msg.payload as Room_State_Payload set_room_id(payload.room_id) set_players(payload.players) set_game_active(payload.game_active) const me = payload.players.find((p) => p.id === payload.your_id) if (me) { set_my_seat(me.seat) } const pmap: Record = {} payload.players.forEach((p) => { pmap[p.seat] = p.name }) set_players_map(pmap) }) const unsub_deal = on('deal_cards', (msg: Message) => { const payload = msg.payload as Deal_Cards_Payload set_hand(sort_cards(payload.cards, payload.level)) set_level(payload.level) set_game_active(true) set_table_cards([]) set_combo_type('') set_selected_ids(new Set()) set_player_card_counts([27, 27, 27, 27]) set_play_log([]) }) const unsub_turn = on('turn', (msg: Message) => { const payload = msg.payload as Turn_Payload set_current_turn(payload.seat) set_can_pass(payload.can_pass) }) const unsub_play_made = on('play_made', (msg: Message) => { const payload = msg.payload as Play_Made_Payload set_play_log((prev) => { const next = [...prev, { seat: payload.seat, cards: payload.cards || [], combo_type: payload.combo_type || '', is_pass: payload.is_pass, }] return next.slice(-8) }) set_last_play_seat(payload.seat) setTimeout(() => set_last_play_seat(null), 800) if (!payload.is_pass) { set_table_cards(payload.cards) set_combo_type(payload.combo_type) set_player_card_counts((prev) => { const next = [...prev] next[payload.seat] -= payload.cards.length return next as [number, number, number, number] }) const played_ids = new Set(payload.cards.map((c) => c.Id)) set_hand((prev) => prev.filter((c) => !played_ids.has(c.Id))) } }) const unsub_hand_end = on('hand_end', (msg: Message) => { const payload = msg.payload as { new_levels: [number, number] } set_team_levels(payload.new_levels) }) const unsub_error = on('error', (msg: Message) => { const payload = msg.payload as Error_Payload set_error(payload.message) setTimeout(() => set_error(null), 3000) }) return () => { unsub_room_state() unsub_deal() unsub_turn() unsub_play_made() unsub_hand_end() unsub_error() } }, [on]) const handle_create_room = useCallback( (name: string) => { send({ type: 'create_room', payload: { player_name: name }, }) }, [send] ) const handle_join_room = useCallback( (room_code: string, name: string) => { send({ type: 'join_room', payload: { room_id: room_code, player_name: name }, }) }, [send] ) const handle_fill_bots = useCallback(() => { send({ type: 'fill_bots', payload: {} }) }, [send]) const handle_card_click = useCallback((id: number) => { set_selected_ids((prev) => { const next = new Set(prev) if (next.has(id)) { next.delete(id) } else { next.add(id) } return next }) }, []) const handle_play = useCallback(() => { if (selected_ids.size === 0) return send({ type: 'play_cards', payload: { card_ids: Array.from(selected_ids) }, }) set_selected_ids(new Set()) }, [send, selected_ids]) const handle_pass = useCallback(() => { send({ type: 'pass', payload: {} }) }, [send]) if (!connected) { return (
Connecting...
) } if (!game_active) { return ( <> {error &&
{error}
} ) } return ( <> {error &&
{error}
} ) } function sort_cards(cards: Card[], level: Rank): Card[] { return [...cards].sort((a, b) => { const va = card_sort_value(a, level) const vb = card_sort_value(b, level) if (va !== vb) return va - vb return a.Suit - b.Suit }) } function card_sort_value(card: Card, level: Rank): number { if (card.Rank === 14) return 100 if (card.Rank === 13) return 99 if (card.Rank === level) return 98 const base_order = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] return base_order[card.Rank] ?? 0 } const styles: Record = { connecting: { display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', backgroundColor: '#1a1a2e', color: '#fff', fontSize: 24, }, error: { position: 'fixed', bottom: 20, left: '50%', transform: 'translateX(-50%)', padding: '12px 24px', backgroundColor: '#dc3545', color: '#fff', borderRadius: 8, zIndex: 1000, }, }