guandan.dev

guandan.dev

https://git.tonybtw.com/guandan.dev.git git://git.tonybtw.com/guandan.dev.git

Fixed size for mobile

Commit
f496a6496824da2d3529cf452c88be9491e18db9
Parent
8520473
Author
tonybanters <tonybanters@gmail.com>
Date
2026-03-22 13:59:15

Diff

diff --git a/client/src/App.tsx b/client/src/App.tsx
index 2824cb2..78356c3 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -11,11 +11,6 @@ import {
 } from './game/types'
 import {
   find_same_rank,
-  select_pair,
-  select_triple,
-  select_bomb,
-  find_valid_plays,
-  detect_combo,
 } from './game/combos'
 
 interface Deal_Cards_Payload {
@@ -199,43 +194,6 @@ export default function App() {
     })
   }, [])
 
-  const handle_quick_select = useCallback((type: 'pair' | 'triple' | 'bomb' | 'clear') => {
-    if (type === 'clear') {
-      set_selected_ids(new Set())
-      return
-    }
-
-    set_hand((current_hand) => {
-      let cards: Card[] | null = null
-      switch (type) {
-        case 'pair':
-          cards = select_pair(current_hand, level)
-          break
-        case 'triple':
-          cards = select_triple(current_hand, level)
-          break
-        case 'bomb':
-          cards = select_bomb(current_hand, level)
-          break
-      }
-      if (cards) {
-        set_selected_ids(new Set(cards.map(c => c.Id)))
-      }
-      return current_hand
-    })
-  }, [level])
-
-  const handle_suggest = useCallback(() => {
-    set_hand((current_hand) => {
-      const table_combo = table_cards.length > 0 ? detect_combo(table_cards, level) : null
-      const suggestions = find_valid_plays(current_hand, table_combo, level)
-      if (suggestions.length > 0) {
-        set_selected_ids(new Set(suggestions[0].map(c => c.Id)))
-      }
-      return current_hand
-    })
-  }, [table_cards, level])
-
   const handle_play = useCallback(() => {
     if (selected_ids.size === 0) return
 
@@ -282,8 +240,6 @@ export default function App() {
         selected_ids={selected_ids}
         on_card_click={handle_card_click}
         on_select_same_rank={handle_select_same_rank}
-        on_quick_select={handle_quick_select}
-        on_suggest={handle_suggest}
         on_play={handle_play}
         on_pass={handle_pass}
         table_cards={table_cards}
diff --git a/client/src/components/Card.tsx b/client/src/components/Card.tsx
index 7c334c9..afa4ce4 100644
--- a/client/src/components/Card.tsx
+++ b/client/src/components/Card.tsx
@@ -1,110 +1,142 @@
-import { motion } from 'framer-motion'
-import { Card as Card_Type, get_suit_symbol, get_rank_symbol, is_red_suit, is_wild, Rank, Rank_Red_Joker, Suit_Joker } from '../game/types'
+import {
+    Card as Card_Type, get_suit_symbol, get_rank_symbol, is_red_suit, is_wild, Rank, Rank_Red_Joker, Suit_Joker
+} from '../game/types'
 
 type Card_Size = 'small' | 'normal'
 
 interface Card_Props {
-  card: Card_Type
-  level: Rank
-  selected: boolean
-  on_click: () => void
-  size?: Card_Size
+    card: Card_Type
+    level: Rank
+    selected: boolean
+    on_click: () => void
+    size?: Card_Size
 }
 
 const SIZE_CONFIG = {
-  small: { width: 56, height: 80, rank_font: 14, suit_font: 16, center_font: 20, corner_rank: 12, corner_suit: 10 },
-  normal: { width: 70, height: 100, rank_font: 16, suit_font: 18, center_font: 24, corner_rank: 14, corner_suit: 12 },
+    small: { width: 36, height: 50, rank_font: 13, suit_font: 11 },
+    normal: { width: 56, height: 78, rank_font: 20, suit_font: 18 },
 }
 
 export function Card({ card, level, selected, on_click, size = 'normal' }: Card_Props) {
-  const is_joker = card.Suit === Suit_Joker
-  const is_red = is_joker ? card.Rank === Rank_Red_Joker : is_red_suit(card.Suit)
-  const is_wild_card = is_wild(card, level)
-  const cfg = SIZE_CONFIG[size]
+    const is_joker = card.Suit === Suit_Joker
+    const is_red = is_joker ? card.Rank === Rank_Red_Joker : is_red_suit(card.Suit)
+    const is_wild_card = is_wild(card, level)
+    const cfg = SIZE_CONFIG[size]
 
-  return (
-    <motion.div
-      onClick={on_click}
-      animate={{
-        y: selected ? -20 : 0,
-        scale: selected ? 1.05 : 1,
-      }}
-      whileHover={{ scale: 1.08 }}
-      transition={{ type: 'spring', stiffness: 400, damping: 25 }}
-      style={{
-        width: cfg.width,
-        height: cfg.height,
-        backgroundColor: is_wild_card ? '#fff3cd' : '#fff',
-        border: is_wild_card ? '3px solid #ffc107' : '2px solid #333',
-        borderRadius: 8,
-        position: 'relative',
-        cursor: 'pointer',
-        userSelect: 'none',
-        boxShadow: selected ? '0 8px 16px rgba(0,0,0,0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
-        color: is_red ? '#dc3545' : '#000',
-        fontWeight: 'bold',
-      }}
-    >
-      <div style={{
-        position: 'absolute',
-        top: 4,
-        left: 6,
-        display: 'flex',
-        flexDirection: 'column',
-        alignItems: 'center',
-        lineHeight: 1,
-      }}>
-        <span style={{ fontSize: cfg.rank_font, fontWeight: 'bold' }}>{is_joker ? (card.Rank === Rank_Red_Joker ? 'R' : 'B') : get_rank_symbol(card.Rank)}</span>
-        <span style={{ fontSize: cfg.suit_font }}>{is_joker ? '🃏' : get_suit_symbol(card.Suit)}</span>
-      </div>
-      <div style={{
-        position: 'absolute',
-        bottom: 4,
-        right: 6,
-        display: 'flex',
-        flexDirection: 'column',
-        alignItems: 'center',
-        lineHeight: 1,
-        transform: 'rotate(180deg)',
-      }}>
-        <span style={{ fontSize: cfg.corner_rank }}>{is_joker ? (card.Rank === Rank_Red_Joker ? 'R' : 'B') : get_rank_symbol(card.Rank)}</span>
-        <span style={{ fontSize: cfg.corner_suit }}>{is_joker ? '🃏' : get_suit_symbol(card.Suit)}</span>
-      </div>
-      <div style={{
-        position: 'absolute',
-        top: '50%',
-        left: '50%',
-        transform: 'translate(-50%, -50%)',
-        fontSize: is_joker ? cfg.center_font + 4 : cfg.center_font,
-      }}>
-        {is_joker ? '🃏' : get_suit_symbol(card.Suit)}
-      </div>
-    </motion.div>
-  )
+    const rank_display = is_joker
+        ? (card.Rank === Rank_Red_Joker ? 'R' : 'B')
+        : get_rank_symbol(card.Rank)
+
+    const suit_display = is_joker ? '🃏' : get_suit_symbol(card.Suit)
+
+    return (
+        <div
+            onClick={on_click}
+            style={{
+                width: cfg.width,
+                height: cfg.height,
+                backgroundColor: is_wild_card ? '#fff3cd' : '#fff',
+                border: is_wild_card ? '2px solid #ffc107' : '1px solid #ccc',
+                borderRadius: 6,
+                position: 'relative',
+                cursor: 'pointer',
+                userSelect: 'none',
+                boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
+                overflow: 'hidden',
+            }}
+        >
+            {/* Selection overlay */}
+            {selected && (
+                <div
+                    style={{
+                        position: 'absolute',
+                        top: 0,
+                        left: 0,
+                        right: 0,
+                        bottom: 0,
+                        backgroundColor: 'rgba(156, 39, 176, 0.35)',
+                        borderRadius: 5,
+                        pointerEvents: 'none',
+                        zIndex: 10,
+                    }}
+                />
+            )}
+
+            {/* Rank and suit at TOP of card so it's visible when stacked */}
+            <div
+                style={{
+                    position: 'absolute',
+                    top: 4,
+                    left: 0,
+                    right: 0,
+                    display: 'flex',
+                    flexDirection: 'row',
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    gap: 1,
+                }}
+            >
+                <span
+                    style={{
+                        fontSize: cfg.rank_font,
+                        fontWeight: 'bold',
+                        color: is_red ? '#dc3545' : '#000',
+                        lineHeight: 1,
+                    }}
+                >
+                    {rank_display}
+                </span>
+                <span
+                    style={{
+                        fontSize: cfg.suit_font,
+                        color: is_red ? '#dc3545' : '#000',
+                        lineHeight: 1,
+                    }}
+                >
+                    {suit_display}
+                </span>
+            </div>
+
+            {/* Big suit in center */}
+            <div
+                style={{
+                    position: 'absolute',
+                    top: '55%',
+                    left: '50%',
+                    transform: 'translate(-50%, -50%)',
+                    fontSize: size === 'small' ? 18 : 32,
+                    color: is_red ? '#dc3545' : '#000',
+                    opacity: 0.9,
+                }}
+            >
+                {suit_display}
+            </div>
+        </div>
+    )
 }
 
 interface Card_Back_Props {
-  size?: Card_Size
+    size?: Card_Size
 }
 
 export function Card_Back({ size = 'normal' }: Card_Back_Props) {
-  const cfg = SIZE_CONFIG[size]
+    const cfg = SIZE_CONFIG[size]
 
-  return (
-    <div
-      style={{
-        width: cfg.width,
-        height: cfg.height,
-        backgroundColor: '#1e3a5f',
-        border: '2px solid #0d1b2a',
-        borderRadius: 8,
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,0.1) 5px, rgba(255,255,255,0.1) 10px)',
-      }}
-    >
-      <div style={{ color: '#fff', fontSize: size === 'small' ? 20 : 24 }}>🀄</div>
-    </div>
-  )
+    return (
+        <div
+            style={{
+                width: cfg.width,
+                height: cfg.height,
+                backgroundColor: '#1e3a5f',
+                border: '1px solid #0d1b2a',
+                borderRadius: 6,
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+                backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,0.1) 5px, rgba(255,255,255,0.1) 10px)',
+            }}
+        >
+            <div style={{ color: '#fff', fontSize: size === 'small' ? 14 : 22 }}>🀄</div>
+        </div>
+    )
 }
diff --git a/client/src/components/Game.tsx b/client/src/components/Game.tsx
index 462ca9f..f1ce23f 100644
--- a/client/src/components/Game.tsx
+++ b/client/src/components/Game.tsx
@@ -11,8 +11,6 @@ interface Game_Props {
   selected_ids: Set<number>
   on_card_click: (id: number) => void
   on_select_same_rank: (rank: number) => void
-  on_quick_select: (type: 'pair' | 'triple' | 'bomb' | 'clear') => void
-  on_suggest: () => void
   on_play: () => void
   on_pass: () => void
   table_cards: Card_Type[]
@@ -32,8 +30,6 @@ export function Game({
   selected_ids,
   on_card_click,
   on_select_same_rank,
-  on_quick_select,
-  on_suggest,
   on_play,
   on_pass,
   table_cards,
@@ -76,48 +72,19 @@ export function Game({
         </div>
 
         <div style={mobile_styles.my_area}>
-          {/* Quick select buttons */}
-          <div style={mobile_styles.quick_select_row}>
-            <button onClick={() => on_quick_select('pair')} style={mobile_styles.quick_btn}>2x</button>
-            <button onClick={() => on_quick_select('triple')} style={mobile_styles.quick_btn}>3x</button>
-            <button onClick={() => on_quick_select('bomb')} style={mobile_styles.quick_btn_bomb}>Bomb</button>
-            <button onClick={on_suggest} style={mobile_styles.quick_btn_suggest}>Hint</button>
-            <button onClick={() => on_quick_select('clear')} style={mobile_styles.quick_btn_clear}>Clear</button>
-          </div>
-
           <Hand
             cards={hand}
             level={level}
             selected_ids={selected_ids}
             on_card_click={on_card_click}
+            on_toggle_selection={on_card_click}
             on_select_same_rank={on_select_same_rank}
+            on_play={on_play}
+            on_pass={on_pass}
+            is_my_turn={is_my_turn}
+            can_pass={can_pass}
           />
 
-          <div style={mobile_styles.actions}>
-            <motion.button
-              whileTap={{ scale: 0.95 }}
-              onClick={on_play}
-              disabled={!is_my_turn || selected_ids.size === 0 || hand.length === 0}
-              style={{
-                ...mobile_styles.action_button,
-                backgroundColor: is_my_turn && selected_ids.size > 0 && hand.length > 0 ? '#28a745' : '#444',
-              }}
-            >
-              Play
-            </motion.button>
-            <motion.button
-              whileTap={{ scale: 0.95 }}
-              onClick={on_pass}
-              disabled={!is_my_turn || !can_pass || hand.length === 0}
-              style={{
-                ...mobile_styles.action_button,
-                backgroundColor: is_my_turn && can_pass && hand.length > 0 ? '#dc3545' : '#444',
-              }}
-            >
-              Pass
-            </motion.button>
-          </div>
-
           {is_my_turn && hand.length > 0 && (
             <motion.div
               initial={{ opacity: 0 }}
@@ -172,7 +139,6 @@ export function Game({
                 is_turn={current_turn === relative_positions.left}
                 just_played={last_play_seat === relative_positions.left}
                 seat={relative_positions.left}
-                vertical
                 name={players_map[relative_positions.left]}
               />
             </div>
@@ -182,62 +148,30 @@ export function Game({
             </div>
 
             <div style={styles.opponent_side}>
-            <Opponent_Hand
-              count={player_card_counts[relative_positions.right]}
-              is_turn={current_turn === relative_positions.right}
-              just_played={last_play_seat === relative_positions.right}
-              seat={relative_positions.right}
-              vertical
-              name={players_map[relative_positions.right]}
-            />
-          </div>
+              <Opponent_Hand
+                count={player_card_counts[relative_positions.right]}
+                is_turn={current_turn === relative_positions.right}
+                just_played={last_play_seat === relative_positions.right}
+                seat={relative_positions.right}
+                name={players_map[relative_positions.right]}
+              />
+            </div>
           </div>
 
           <div style={styles.my_area}>
-            {/* Quick select buttons */}
-            <div style={styles.quick_select_row}>
-              <button onClick={() => on_quick_select('pair')} style={styles.quick_btn}>Pair</button>
-              <button onClick={() => on_quick_select('triple')} style={styles.quick_btn}>Triple</button>
-              <button onClick={() => on_quick_select('bomb')} style={styles.quick_btn_bomb}>Bomb</button>
-              <button onClick={on_suggest} style={styles.quick_btn_suggest}>Suggest</button>
-              <button onClick={() => on_quick_select('clear')} style={styles.quick_btn_clear}>Clear</button>
-            </div>
-
             <Hand
               cards={hand}
               level={level}
               selected_ids={selected_ids}
               on_card_click={on_card_click}
+              on_toggle_selection={on_card_click}
               on_select_same_rank={on_select_same_rank}
+              on_play={on_play}
+              on_pass={on_pass}
+              is_my_turn={is_my_turn}
+              can_pass={can_pass}
             />
 
-          <div style={styles.actions}>
-            <motion.button
-              whileHover={{ scale: 1.05 }}
-              whileTap={{ scale: 0.95 }}
-              onClick={on_play}
-              disabled={!is_my_turn || selected_ids.size === 0 || hand.length === 0}
-              style={{
-                ...styles.action_button,
-                backgroundColor: is_my_turn && selected_ids.size > 0 && hand.length > 0 ? '#28a745' : '#444',
-              }}
-            >
-              Play
-            </motion.button>
-            <motion.button
-              whileHover={{ scale: 1.05 }}
-              whileTap={{ scale: 0.95 }}
-              onClick={on_pass}
-              disabled={!is_my_turn || !can_pass || hand.length === 0}
-              style={{
-                ...styles.action_button,
-                backgroundColor: is_my_turn && can_pass && hand.length > 0 ? '#dc3545' : '#444',
-              }}
-            >
-              Pass
-            </motion.button>
-          </div>
-
           {is_my_turn && hand.length > 0 && (
             <motion.div
               initial={{ opacity: 0 }}
@@ -307,14 +241,10 @@ interface Opponent_Hand_Props {
   is_turn: boolean
   just_played?: boolean
   seat: number
-  vertical?: boolean
   name?: string
 }
 
-function Opponent_Hand({ count, is_turn, just_played, seat, vertical, name }: Opponent_Hand_Props) {
-  const display_count = Math.min(count, 10)
-  const overlap = vertical ? 15 : 20
-
+function Opponent_Hand({ count, is_turn, just_played, seat, name }: Opponent_Hand_Props) {
   const get_highlight_style = () => {
     if (just_played) return { backgroundColor: 'rgba(76, 175, 80, 0.3)', border: '2px solid #4caf50' }
     if (is_turn) return { backgroundColor: 'rgba(255,193,7,0.2)', border: '2px solid #ffc107' }
@@ -325,41 +255,38 @@ function Opponent_Hand({ count, is_turn, just_played, seat, vertical, name }: Op
     <div
       style={{
         display: 'flex',
-        flexDirection: vertical ? 'column' : 'row',
+        flexDirection: 'column',
         alignItems: 'center',
-        gap: 8,
+        gap: 4,
         padding: 8,
         borderRadius: 8,
         transition: 'all 0.2s ease',
         ...get_highlight_style(),
       }}
     >
-      <div
-        style={{
-          display: 'flex',
-          flexDirection: vertical ? 'column' : 'row',
-          position: 'relative',
-          width: vertical ? 50 : 50 + (display_count - 1) * overlap,
-          height: vertical ? 70 + (display_count - 1) * overlap : 70,
-        }}
-      >
-        {Array.from({ length: display_count }).map((_, i) => (
-          <div
-            key={i}
-            style={{
-              position: 'absolute',
-              left: vertical ? 0 : i * overlap,
-              top: vertical ? i * overlap : 0,
-              transform: 'scale(0.7)',
-              transformOrigin: 'top left',
-            }}
-          >
-            <Card_Back />
-          </div>
-        ))}
+      <div style={{ position: 'relative' }}>
+        <Card_Back size="small" />
+        <div
+          style={{
+            position: 'absolute',
+            top: '50%',
+            left: '50%',
+            transform: 'translate(-50%, -50%)',
+            backgroundColor: 'rgba(0,0,0,0.7)',
+            color: '#fff',
+            fontWeight: 'bold',
+            fontSize: 16,
+            padding: '4px 8px',
+            borderRadius: 4,
+            minWidth: 24,
+            textAlign: 'center',
+          }}
+        >
+          {count}
+        </div>
       </div>
-      <div style={{ color: '#fff', fontSize: 12 }}>
-        {name || `Seat ${seat + 1}`}: {count}
+      <div style={{ color: '#fff', fontSize: 11 }}>
+        {name || `P${seat + 1}`}
       </div>
     </div>
   )
@@ -373,15 +300,6 @@ function get_relative_positions(my_seat: number) {
   }
 }
 
-const quick_btn_base: React.CSSProperties = {
-  padding: '6px 12px',
-  fontSize: 12,
-  border: 'none',
-  borderRadius: 6,
-  cursor: 'pointer',
-  fontWeight: 'bold',
-}
-
 const styles: Record<string, React.CSSProperties> = {
   container: {
     display: 'flex',
@@ -406,15 +324,6 @@ const styles: Record<string, React.CSSProperties> = {
     fontWeight: 'bold',
     fontSize: 14,
   },
-  layout_toggle: {
-    padding: '6px 12px',
-    backgroundColor: '#4a5568',
-    color: '#fff',
-    border: 'none',
-    borderRadius: 6,
-    cursor: 'pointer',
-    fontSize: 12,
-  },
   team_scores: {
     color: '#fff',
     fontSize: 12,
@@ -462,44 +371,6 @@ const styles: Record<string, React.CSSProperties> = {
     borderTop: '2px solid #333',
     flexShrink: 0,
   },
-  quick_select_row: {
-    display: 'flex',
-    gap: 8,
-    marginBottom: 8,
-  },
-  quick_btn: {
-    ...quick_btn_base,
-    backgroundColor: '#2196f3',
-    color: '#fff',
-  },
-  quick_btn_bomb: {
-    ...quick_btn_base,
-    backgroundColor: '#ff5722',
-    color: '#fff',
-  },
-  quick_btn_suggest: {
-    ...quick_btn_base,
-    backgroundColor: '#9c27b0',
-    color: '#fff',
-  },
-  quick_btn_clear: {
-    ...quick_btn_base,
-    backgroundColor: '#607d8b',
-    color: '#fff',
-  },
-  actions: {
-    display: 'flex',
-    gap: 12,
-    marginTop: 8,
-  },
-  action_button: {
-    padding: '10px 24px',
-    fontSize: 14,
-    border: 'none',
-    borderRadius: 8,
-    color: '#fff',
-    cursor: 'pointer',
-  },
   turn_indicator: {
     marginTop: 8,
     padding: '6px 12px',
@@ -517,15 +388,6 @@ const styles: Record<string, React.CSSProperties> = {
   },
 }
 
-const mobile_quick_btn_base: React.CSSProperties = {
-  padding: '4px 8px',
-  fontSize: 10,
-  border: 'none',
-  borderRadius: 4,
-  cursor: 'pointer',
-  fontWeight: 'bold',
-}
-
 const mobile_styles: Record<string, React.CSSProperties> = {
   container: {
     display: 'flex',
@@ -550,15 +412,6 @@ const mobile_styles: Record<string, React.CSSProperties> = {
     fontWeight: 'bold',
     fontSize: 12,
   },
-  layout_toggle: {
-    padding: '4px 8px',
-    backgroundColor: '#4a5568',
-    color: '#fff',
-    border: 'none',
-    borderRadius: 4,
-    cursor: 'pointer',
-    fontSize: 12,
-  },
   team_scores: {
     color: '#fff',
     fontSize: 11,
@@ -597,44 +450,6 @@ const mobile_styles: Record<string, React.CSSProperties> = {
     borderTop: '2px solid #333',
     flexShrink: 0,
   },
-  quick_select_row: {
-    display: 'flex',
-    gap: 6,
-    marginBottom: 4,
-  },
-  quick_btn: {
-    ...mobile_quick_btn_base,
-    backgroundColor: '#2196f3',
-    color: '#fff',
-  },
-  quick_btn_bomb: {
-    ...mobile_quick_btn_base,
-    backgroundColor: '#ff5722',
-    color: '#fff',
-  },
-  quick_btn_suggest: {
-    ...mobile_quick_btn_base,
-    backgroundColor: '#9c27b0',
-    color: '#fff',
-  },
-  quick_btn_clear: {
-    ...mobile_quick_btn_base,
-    backgroundColor: '#607d8b',
-    color: '#fff',
-  },
-  actions: {
-    display: 'flex',
-    gap: 16,
-    marginTop: 4,
-  },
-  action_button: {
-    padding: '10px 28px',
-    fontSize: 14,
-    border: 'none',
-    borderRadius: 8,
-    color: '#fff',
-    cursor: 'pointer',
-  },
   turn_indicator: {
     marginTop: 6,
     padding: '4px 10px',
diff --git a/client/src/components/Hand.tsx b/client/src/components/Hand.tsx
index dc40d2c..8bc6262 100644
--- a/client/src/components/Hand.tsx
+++ b/client/src/components/Hand.tsx
@@ -1,6 +1,6 @@
 import { useRef, useCallback, useState } from 'react'
 import { motion, AnimatePresence } from 'framer-motion'
-import { Card as Card_Type, Rank } from '../game/types'
+import { Card as Card_Type, Rank, Suit_Hearts, Suit_Diamonds, Suit_Clubs, Suit_Spades } from '../game/types'
 import { Card } from './Card'
 import { use_is_mobile } from '../hooks/use_is_mobile'
 
@@ -9,7 +9,12 @@ interface Hand_Props {
   level: Rank
   selected_ids: Set<number>
   on_card_click: (id: number) => void
+  on_toggle_selection: (id: number) => void
   on_select_same_rank: (rank: number) => void
+  on_play: () => void
+  on_pass: () => void
+  is_my_turn: boolean
+  can_pass: boolean
 }
 
 interface Column {
@@ -18,23 +23,21 @@ interface Column {
   is_custom: boolean
 }
 
-export function Hand({ cards, level, selected_ids, on_card_click, on_select_same_rank }: Hand_Props) {
+export function Hand({ cards, level, selected_ids, on_card_click, on_toggle_selection, on_select_same_rank, on_play, on_pass, is_my_turn, can_pass }: Hand_Props) {
   const is_mobile = use_is_mobile()
   const last_click = useRef<{ id: number; time: number } | null>(null)
   const [custom_columns, set_custom_columns] = useState<Map<string, number[]>>(new Map())
-  const [drag_card_id, set_drag_card_id] = useState<number | null>(null)
-  const [drop_target, set_drop_target] = useState<string | null>(null)
 
-  // Touch drag state
-  const touch_start_pos = useRef<{ x: number; y: number } | null>(null)
-  const is_touch_dragging = useRef(false)
-  const column_refs = useRef<Map<string, HTMLDivElement>>(new Map())
-  const new_pile_ref = useRef<HTMLDivElement>(null)
+  // Swipe-to-select state
+  const [is_swiping, set_is_swiping] = useState(false)
+  const swipe_start = useRef<{ x: number; y: number } | null>(null)
+  const swiped_cards = useRef<Set<number>>(new Set())
+  const card_refs = useRef<Map<number, HTMLDivElement>>(new Map())
 
   const handle_card_click = useCallback((card: Card_Type) => {
-    // Don't trigger click if we were dragging
-    if (is_touch_dragging.current) {
-      is_touch_dragging.current = false
+    // Don't trigger click if we were swiping
+    if (swiped_cards.current.size > 0) {
+      swiped_cards.current.clear()
       return
     }
 
@@ -57,14 +60,15 @@ export function Hand({ cards, level, selected_ids, on_card_click, on_select_same
   // Filter to only cards that still exist in hand
   const valid_card_ids = new Set(cards.map(c => c.Id))
 
-  // Build columns: custom columns first, then auto-sorted remaining cards
+  // Build columns: auto-sorted cards first, then custom columns on right
   const columns: Column[] = []
+  const custom_cols: Column[] = []
 
-  // Add custom columns (filter out cards no longer in hand)
+  // Collect custom columns (filter out cards no longer in hand)
   custom_columns.forEach((card_ids, col_id) => {
     const valid_ids = card_ids.filter(id => valid_card_ids.has(id))
     if (valid_ids.length > 0) {
-      columns.push({ id: col_id, card_ids: valid_ids, is_custom: true })
+      custom_cols.push({ id: col_id, card_ids: valid_ids, is_custom: true })
     }
   })
 
@@ -97,52 +101,28 @@ export function Hand({ cards, level, selected_ids, on_card_click, on_select_same
     })
   })
 
+  // Add custom columns at the end (right side)
+  columns.push(...custom_cols)
+
   // Card lookup
   const card_by_id = new Map(cards.map(c => [c.Id, c]))
 
-  const card_width = is_mobile ? 44 : 56
-  const card_height = is_mobile ? 62 : 78
-  const v_overlap = is_mobile ? 24 : 30
-  const h_gap = is_mobile ? 3 : 4
-  const selection_lift = 24
-
-  // Find drop target from touch position
-  const find_drop_target = (x: number, y: number): string | null => {
-    // Check new pile zone
-    if (new_pile_ref.current) {
-      const rect = new_pile_ref.current.getBoundingClientRect()
-      if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
-        return 'new'
-      }
-    }
+  const card_width = is_mobile ? 36 : 56
+  const card_height = is_mobile ? 50 : 78
+  const v_overlap = is_mobile ? 18 : 30
+  const h_gap = is_mobile ? 2 : 3
 
-    // Check custom columns
-    for (const [col_id, el] of column_refs.current) {
-      if (col_id.startsWith('custom-')) {
-        const rect = el.getBoundingClientRect()
-        if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
-          return col_id
-        }
-      }
-    }
-
-    return null
-  }
-
-  // Commit the drop
-  const commit_drop = (card_id: number, target: string | null) => {
-    if (target === null) {
-      set_drag_card_id(null)
-      set_drop_target(null)
-      return
-    }
+  // Move selected cards to a new custom pile
+  const handle_create_pile = () => {
+    if (selected_ids.size === 0) return
 
+    const selected_array = Array.from(selected_ids)
     set_custom_columns(prev => {
       const next = new Map(prev)
 
-      // Remove from any existing custom column
+      // Remove selected cards from any existing custom columns
       next.forEach((ids, col_id) => {
-        const filtered = ids.filter(id => id !== card_id)
+        const filtered = ids.filter(id => !selected_ids.has(id))
         if (filtered.length === 0) {
           next.delete(col_id)
         } else {
@@ -150,73 +130,79 @@ export function Hand({ cards, level, selected_ids, on_card_click, on_select_same
         }
       })
 
-      if (target === 'new') {
-        const new_col_id = `custom-${Date.now()}`
-        next.set(new_col_id, [card_id])
-      } else if (target.startsWith('custom-')) {
-        const existing = next.get(target) || []
-        next.set(target, [...existing, card_id])
-      }
+      // Create new column with selected cards
+      const new_col_id = `custom-${Date.now()}`
+      next.set(new_col_id, selected_array)
 
       return next
     })
-
-    set_drag_card_id(null)
-    set_drop_target(null)
   }
 
-  // Mouse drag handlers
-  const handle_drag_start = (card_id: number) => {
-    set_drag_card_id(card_id)
+  // Reset all custom arrangements
+  const handle_reset = () => {
+    set_custom_columns(new Map())
   }
 
-  const handle_drag_end = () => {
-    if (drag_card_id !== null && drop_target !== null) {
-      commit_drop(drag_card_id, drop_target)
-    } else {
-      set_drag_card_id(null)
-      set_drop_target(null)
-    }
-  }
-
-  const handle_drag_over_column = (col_id: string) => {
-    set_drop_target(col_id)
+  // Select all cards of a given suit
+  const handle_select_suit = (suit: number) => {
+    const suit_cards = cards.filter(c => c.Suit === suit)
+    if (suit_cards.length === 0) return
+
+    // Toggle: if all are selected, deselect; otherwise select all
+    const all_selected = suit_cards.every(c => selected_ids.has(c.Id))
+    suit_cards.forEach(c => {
+      if (all_selected) {
+        on_card_click(c.Id) // deselect
+      } else if (!selected_ids.has(c.Id)) {
+        on_toggle_selection(c.Id) // select
+      }
+    })
   }
 
-  const handle_drag_leave = () => {
-    set_drop_target(null)
+  // Swipe-to-select handlers
+  const find_card_at_point = (x: number, y: number): number | null => {
+    for (const [card_id, el] of card_refs.current) {
+      const rect = el.getBoundingClientRect()
+      if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
+        return card_id
+      }
+    }
+    return null
   }
 
-  // Touch handlers
-  const handle_touch_start = (_card_id: number, e: React.TouchEvent) => {
-    const touch = e.touches[0]
-    touch_start_pos.current = { x: touch.clientX, y: touch.clientY }
-    is_touch_dragging.current = false
+  const handle_swipe_start = (e: React.MouseEvent | React.TouchEvent) => {
+    const point = 'touches' in e ? e.touches[0] : e
+    swipe_start.current = { x: point.clientX, y: point.clientY }
+    swiped_cards.current.clear()
+    set_is_swiping(false)
   }
 
-  const handle_touch_move = (card_id: number, e: React.TouchEvent) => {
-    if (!touch_start_pos.current) return
+  const handle_swipe_move = (e: React.MouseEvent | React.TouchEvent) => {
+    if (!swipe_start.current) return
 
-    const touch = e.touches[0]
-    const dx = Math.abs(touch.clientX - touch_start_pos.current.x)
-    const dy = Math.abs(touch.clientY - touch_start_pos.current.y)
+    const point = 'touches' in e ? e.touches[0] : e
+    const dx = Math.abs(point.clientX - swipe_start.current.x)
+    const dy = Math.abs(point.clientY - swipe_start.current.y)
 
-    // Start dragging if moved more than 10px
-    if (dx > 10 || dy > 10) {
-      is_touch_dragging.current = true
-      set_drag_card_id(card_id)
+    // Start swiping if moved more than 5px
+    if (dx > 5 || dy > 5) {
+      set_is_swiping(true)
+    }
 
-      const target = find_drop_target(touch.clientX, touch.clientY)
-      set_drop_target(target)
+    if (is_swiping) {
+      const card_id = find_card_at_point(point.clientX, point.clientY)
+      if (card_id !== null && !swiped_cards.current.has(card_id)) {
+        swiped_cards.current.add(card_id)
+        on_toggle_selection(card_id)
+      }
     }
   }
 
-  const handle_touch_end = (_card_id: number) => {
-    if (is_touch_dragging.current && drag_card_id !== null) {
-      commit_drop(drag_card_id, drop_target)
-    }
-    touch_start_pos.current = null
-    // Don't reset is_touch_dragging here - let click handler check it
+  const handle_swipe_end = () => {
+    swipe_start.current = null
+    set_is_swiping(false)
+    // Don't clear swiped_cards here - let click handler check it
+    setTimeout(() => swiped_cards.current.clear(), 50)
   }
 
   // Double-tap on custom column to dissolve it
@@ -231,120 +217,198 @@ export function Hand({ cards, level, selected_ids, on_card_click, on_select_same
   }
 
   return (
-    <div
-      style={{
+    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '100%' }}>
+      {/* Cards area with swipe detection */}
+      <div
+        onMouseDown={handle_swipe_start}
+        onMouseMove={handle_swipe_move}
+        onMouseUp={handle_swipe_end}
+        onMouseLeave={handle_swipe_end}
+        onTouchStart={handle_swipe_start}
+        onTouchMove={handle_swipe_move}
+        onTouchEnd={handle_swipe_end}
+        style={{
+          display: 'flex',
+          justifyContent: 'center',
+          padding: is_mobile ? '4px 8px' : '8px 16px',
+          overflowX: 'auto',
+          overflowY: 'visible',
+          WebkitOverflowScrolling: 'touch',
+          width: '100%',
+          cursor: is_swiping ? 'crosshair' : 'default',
+          userSelect: 'none',
+        }}
+      >
+        <div style={{ display: 'flex', gap: h_gap, alignItems: 'flex-end' }}>
+          {columns.map((col, col_idx) => {
+            const col_cards = col.card_ids.map(id => card_by_id.get(id)!).filter(Boolean)
+            const col_height = card_height + (col_cards.length - 1) * v_overlap
+
+            return (
+              <div
+                key={col.id}
+                onDoubleClick={() => handle_column_double_click(col.id)}
+                style={{
+                  position: 'relative',
+                  width: card_width,
+                  height: col_height,
+                  borderRadius: 4,
+                  flexShrink: 0,
+                }}
+              >
+                <AnimatePresence>
+                  {col_cards.map((card, card_idx) => {
+                    // First card (idx 0) = bottom position, FRONT (highest z-index)
+                    // Last card = top position, BACK (lowest z-index, only top peeks out)
+                    const from_bottom = card_idx
+
+                    return (
+                      <motion.div
+                        key={card.Id}
+                        ref={(el) => { if (el) card_refs.current.set(card.Id, el) }}
+                        initial={{ opacity: 0, y: 20, scale: 0.8 }}
+                        animate={{ opacity: 1, y: 0, scale: 1 }}
+                        exit={{ opacity: 0, y: -20, scale: 0.8 }}
+                        transition={{ delay: (col_idx * 0.3 + card_idx) * 0.01 }}
+                        style={{
+                          position: 'absolute',
+                          bottom: from_bottom * v_overlap,
+                          left: 0,
+                          zIndex: col_cards.length - card_idx,
+                          cursor: 'pointer',
+                          touchAction: 'none',
+                        }}
+                      >
+                        <Card
+                          card={card}
+                          level={level}
+                          selected={selected_ids.has(card.Id)}
+                          on_click={() => handle_card_click(card)}
+                          size="small"
+                        />
+                      </motion.div>
+                    )
+                  })}
+                </AnimatePresence>
+              </div>
+            )
+          })}
+        </div>
+      </div>
+
+      {/* Suit filter buttons + action buttons */}
+      <div style={{
         display: 'flex',
+        gap: is_mobile ? 6 : 10,
+        marginTop: is_mobile ? 4 : 8,
         justifyContent: 'center',
-        padding: is_mobile ? '4px 8px' : '8px 16px',
-        paddingTop: selection_lift + (is_mobile ? 4 : 8),
-        overflowX: 'auto',
-        overflowY: 'visible',
-        WebkitOverflowScrolling: 'touch',
-        width: '100%',
-      }}
-    >
-      <div style={{ display: 'flex', gap: h_gap, alignItems: 'flex-start' }}>
-        {/* New pile drop zone - always visible */}
-        <div
-          ref={new_pile_ref}
-          onDragOver={(e) => { e.preventDefault(); set_drop_target('new') }}
-          onDragLeave={handle_drag_leave}
-          onDrop={handle_drag_end}
+        alignItems: 'center',
+        flexWrap: 'wrap',
+      }}>
+        {/* Suit buttons */}
+        <div style={{ display: 'flex', gap: is_mobile ? 4 : 6 }}>
+          {[
+            { suit: Suit_Spades, symbol: '♠', color: '#000' },
+            { suit: Suit_Hearts, symbol: '♥', color: '#dc3545' },
+            { suit: Suit_Clubs, symbol: '♣', color: '#000' },
+            { suit: Suit_Diamonds, symbol: '♦', color: '#dc3545' },
+          ].map(({ suit, symbol, color }) => (
+            <button
+              key={suit}
+              onClick={() => handle_select_suit(suit)}
+              style={{
+                width: is_mobile ? 28 : 34,
+                height: is_mobile ? 28 : 34,
+                fontSize: is_mobile ? 16 : 20,
+                backgroundColor: '#fff',
+                color: color,
+                border: '1px solid #ccc',
+                borderRadius: 6,
+                cursor: 'pointer',
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+              }}
+            >
+              {symbol}
+            </button>
+          ))}
+        </div>
+
+        {/* Divider */}
+        <div style={{ width: 1, height: is_mobile ? 20 : 24, backgroundColor: '#555' }} />
+
+        {/* Action buttons */}
+        <button
+          onClick={handle_create_pile}
+          disabled={selected_ids.size === 0}
           style={{
-            width: card_width,
-            height: card_height,
-            border: `2px dashed ${drop_target === 'new' ? '#4caf50' : drag_card_id !== null ? '#888' : '#444'}`,
-            borderRadius: 8,
-            display: 'flex',
-            alignItems: 'center',
-            justifyContent: 'center',
-            color: drop_target === 'new' ? '#4caf50' : drag_card_id !== null ? '#888' : '#555',
-            fontSize: is_mobile ? 18 : 24,
-            backgroundColor: drop_target === 'new' ? 'rgba(76,175,80,0.15)' : 'transparent',
-            opacity: drag_card_id !== null ? 1 : 0.4,
-            transition: 'all 0.15s ease',
-            flexShrink: 0,
+            padding: is_mobile ? '6px 10px' : '8px 14px',
+            fontSize: is_mobile ? 11 : 13,
+            backgroundColor: selected_ids.size > 0 ? '#9c27b0' : '#444',
+            color: '#fff',
+            border: 'none',
+            borderRadius: 6,
+            cursor: selected_ids.size > 0 ? 'pointer' : 'default',
+            opacity: selected_ids.size > 0 ? 1 : 0.5,
           }}
         >
-          +
-        </div>
+          New Pile
+        </button>
+        <button
+          onClick={handle_reset}
+          disabled={custom_columns.size === 0}
+          style={{
+            padding: is_mobile ? '6px 10px' : '8px 14px',
+            fontSize: is_mobile ? 11 : 13,
+            backgroundColor: custom_columns.size > 0 ? '#607d8b' : '#444',
+            color: '#fff',
+            border: 'none',
+            borderRadius: 6,
+            cursor: custom_columns.size > 0 ? 'pointer' : 'default',
+            opacity: custom_columns.size > 0 ? 1 : 0.5,
+          }}
+        >
+          Reset
+        </button>
 
-        {columns.map((col, col_idx) => {
-          const col_cards = col.card_ids.map(id => card_by_id.get(id)!).filter(Boolean)
-          const col_height = card_height + (col_cards.length - 1) * v_overlap
-
-          return (
-            <div
-              key={col.id}
-              ref={(el) => { if (el) column_refs.current.set(col.id, el) }}
-              onDragOver={(e) => { e.preventDefault(); if (col.is_custom) handle_drag_over_column(col.id) }}
-              onDragLeave={handle_drag_leave}
-              onDrop={handle_drag_end}
-              onDoubleClick={() => handle_column_double_click(col.id)}
-              style={{
-                position: 'relative',
-                width: card_width,
-                height: col_height,
-                borderLeft: col.is_custom ? '2px solid #9c27b0' : 'none',
-                paddingLeft: col.is_custom ? 2 : 0,
-                backgroundColor: drop_target === col.id ? 'rgba(156,39,176,0.15)' : 'transparent',
-                borderRadius: 4,
-                flexShrink: 0,
-              }}
-            >
-              {col.is_custom && (
-                <div
-                  style={{
-                    position: 'absolute',
-                    top: -14,
-                    left: 0,
-                    fontSize: 8,
-                    color: '#9c27b0',
-                  }}
-                >
-                  ✦
-                </div>
-              )}
-              <AnimatePresence>
-                {col_cards.map((card, card_idx) => (
-                  <motion.div
-                    key={card.Id}
-                    draggable
-                    onDragStart={() => handle_drag_start(card.Id)}
-                    onDragEnd={handle_drag_end}
-                    onTouchStart={(e) => handle_touch_start(card.Id, e)}
-                    onTouchMove={(e) => handle_touch_move(card.Id, e)}
-                    onTouchEnd={() => handle_touch_end(card.Id)}
-                    initial={{ opacity: 0, x: 20, scale: 0.8 }}
-                    animate={{
-                      opacity: drag_card_id === card.Id ? 0.5 : 1,
-                      x: 0,
-                      scale: 1
-                    }}
-                    exit={{ opacity: 0, x: -20, scale: 0.8 }}
-                    transition={{ delay: (col_idx * 0.3 + card_idx) * 0.01 }}
-                    style={{
-                      position: 'absolute',
-                      top: card_idx * v_overlap,
-                      left: 0,
-                      zIndex: card_idx,
-                      cursor: 'grab',
-                      touchAction: 'none',
-                    }}
-                  >
-                    <Card
-                      card={card}
-                      level={level}
-                      selected={selected_ids.has(card.Id)}
-                      on_click={() => handle_card_click(card)}
-                      size="small"
-                    />
-                  </motion.div>
-                ))}
-              </AnimatePresence>
-            </div>
-          )
-        })}
+        {/* Divider */}
+        <div style={{ width: 1, height: is_mobile ? 20 : 24, backgroundColor: '#555' }} />
+
+        {/* Play/Pass buttons */}
+        <button
+          onClick={on_pass}
+          disabled={!is_my_turn || !can_pass || cards.length === 0}
+          style={{
+            padding: is_mobile ? '6px 12px' : '8px 16px',
+            fontSize: is_mobile ? 12 : 14,
+            backgroundColor: is_my_turn && can_pass && cards.length > 0 ? '#dc3545' : '#444',
+            color: '#fff',
+            border: 'none',
+            borderRadius: 6,
+            cursor: is_my_turn && can_pass && cards.length > 0 ? 'pointer' : 'default',
+            opacity: is_my_turn && can_pass && cards.length > 0 ? 1 : 0.5,
+          }}
+        >
+          Pass
+        </button>
+        <button
+          onClick={on_play}
+          disabled={!is_my_turn || selected_ids.size === 0 || cards.length === 0}
+          style={{
+            padding: is_mobile ? '6px 16px' : '8px 24px',
+            fontSize: is_mobile ? 13 : 15,
+            fontWeight: 'bold',
+            backgroundColor: is_my_turn && selected_ids.size > 0 && cards.length > 0 ? '#28a745' : '#444',
+            color: '#fff',
+            border: 'none',
+            borderRadius: 6,
+            cursor: is_my_turn && selected_ids.size > 0 && cards.length > 0 ? 'pointer' : 'default',
+            opacity: is_my_turn && selected_ids.size > 0 && cards.length > 0 ? 1 : 0.5,
+          }}
+        >
+          Play
+        </button>
       </div>
     </div>
   )
diff --git a/client/src/components/Lobby.tsx b/client/src/components/Lobby.tsx
index 5eeb87f..0ac05a8 100644
--- a/client/src/components/Lobby.tsx
+++ b/client/src/components/Lobby.tsx
@@ -3,269 +3,269 @@ import { motion } from 'framer-motion'
 import { Player_Info } from '../game/types'
 
 interface Lobby_Props {
-  room_id: string | null
-  players: Player_Info[]
-  on_create_room: (name: string) => void
-  on_join_room: (room_id: string, name: string) => void
-  on_fill_bots: () => void
+    room_id: string | null
+    players: Player_Info[]
+    on_create_room: (name: string) => void
+    on_join_room: (room_id: string, name: string) => void
+    on_fill_bots: () => void
 }
 
 export function Lobby({ room_id, players, on_create_room, on_join_room, on_fill_bots }: Lobby_Props) {
-  const [name, set_name] = useState('')
-  const [join_code, set_join_code] = useState('')
-  const [mode, set_mode] = useState<'select' | 'create' | 'join'>('select')
+    const [name, set_name] = useState('')
+    const [join_code, set_join_code] = useState('')
+    const [mode, set_mode] = useState<'select' | 'create' | 'join'>('select')
 
-  const handle_create = () => {
-    if (name.trim()) {
-      on_create_room(name.trim())
+    const handle_create = () => {
+        if (name.trim()) {
+            on_create_room(name.trim())
+        }
     }
-  }
 
-  const handle_join = () => {
-    if (name.trim() && join_code.trim()) {
-      on_join_room(join_code.trim(), name.trim())
+    const handle_join = () => {
+        if (name.trim() && join_code.trim()) {
+            on_join_room(join_code.trim(), name.trim())
+        }
     }
-  }
 
-  if (room_id) {
-    return (
-      <div style={styles.container}>
-        <motion.div
-          initial={{ opacity: 0, y: 20 }}
-          animate={{ opacity: 1, y: 0 }}
-          style={styles.card}
-        >
-          <h2 style={styles.title}>Room: {room_id}</h2>
-          <p style={styles.subtitle}>Waiting for players... ({players.length}/4)</p>
-
-          <div style={styles.players_grid}>
-            {[0, 1, 2, 3].map((seat) => {
-              const player = players.find((p) => p.seat === seat)
-              const team = seat % 2
-              return (
+    if (room_id) {
+        return (
+            <div style={styles.container}>
                 <motion.div
-                  key={seat}
-                  initial={{ opacity: 0, scale: 0.8 }}
-                  animate={{ opacity: 1, scale: 1 }}
-                  transition={{ delay: seat * 0.1 }}
-                  style={{
-                    ...styles.player_slot,
-                    backgroundColor: team === 0 ? '#e3f2fd' : '#fce4ec',
-                    borderColor: team === 0 ? '#2196f3' : '#e91e63',
-                  }}
+                    initial={{ opacity: 0, y: 20 }}
+                    animate={{ opacity: 1, y: 0 }}
+                    style={styles.card}
                 >
-                  {player ? (
-                    <>
-                      <div style={styles.player_name}>{player.name}</div>
-                      <div style={styles.player_team}>Team {team + 1}</div>
-                    </>
-                  ) : (
-                    <div style={styles.empty_slot}>Empty</div>
-                  )}
-                </motion.div>
-              )
-            })}
-          </div>
+                    <h2 style={styles.title}>Room: {room_id}</h2>
+                    <p style={styles.subtitle}>Waiting for players... ({players.length}/4)</p>
 
-          <motion.button
-            whileHover={{ scale: 1.05 }}
-            whileTap={{ scale: 0.95 }}
-            onClick={on_fill_bots}
-            style={{ ...styles.button, backgroundColor: '#ff9800', marginBottom: 16 }}
-          >
-            Fill with Bots
-          </motion.button>
+                    <div style={styles.players_grid}>
+                        {[0, 1, 2, 3].map((seat) => {
+                            const player = players.find((p) => p.seat === seat)
+                            const team = seat % 2
+                            return (
+                                <motion.div
+                                    key={seat}
+                                    initial={{ opacity: 0, scale: 0.8 }}
+                                    animate={{ opacity: 1, scale: 1 }}
+                                    transition={{ delay: seat * 0.1 }}
+                                    style={{
+                                        ...styles.player_slot,
+                                        backgroundColor: team === 0 ? '#e3f2fd' : '#fce4ec',
+                                        borderColor: team === 0 ? '#2196f3' : '#e91e63',
+                                    }}
+                                >
+                                    {player ? (
+                                        <>
+                                            <div style={styles.player_name}>{player.name}</div>
+                                            <div style={styles.player_team}>Team {team + 1}</div>
+                                        </>
+                                    ) : (
+                                        <div style={styles.empty_slot}>Empty</div>
+                                    )}
+                                </motion.div>
+                            )
+                        })}
+                    </div>
 
-          <p style={styles.hint}>Share room code with friends to join</p>
-        </motion.div>
-      </div>
-    )
-  }
+                    <motion.button
+                        whileHover={{ scale: 1.05 }}
+                        whileTap={{ scale: 0.95 }}
+                        onClick={on_fill_bots}
+                        style={{ ...styles.button, backgroundColor: '#ff9800', marginBottom: 16 }}
+                    >
+                        Fill with Bots
+                    </motion.button>
 
-  return (
-    <div style={styles.container}>
-      <motion.div
-        initial={{ opacity: 0, y: 20 }}
-        animate={{ opacity: 1, y: 0 }}
-        style={styles.card}
-      >
-        <h1 style={styles.logo}>掼蛋</h1>
-        <h2 style={styles.title}>Guan Dan</h2>
+                    <p style={styles.hint}>Share room code with friends to join</p>
+                </motion.div>
+            </div>
+        )
+    }
 
-        {mode === 'select' && (
-          <div style={styles.buttons}>
-            <motion.button
-              whileHover={{ scale: 1.05 }}
-              whileTap={{ scale: 0.95 }}
-              onClick={() => set_mode('create')}
-              style={styles.button}
-            >
-              Create Room
-            </motion.button>
-            <motion.button
-              whileHover={{ scale: 1.05 }}
-              whileTap={{ scale: 0.95 }}
-              onClick={() => set_mode('join')}
-              style={{ ...styles.button, backgroundColor: '#28a745' }}
+    return (
+        <div style={styles.container}>
+            <motion.div
+                initial={{ opacity: 0, y: 20 }}
+                animate={{ opacity: 1, y: 0 }}
+                style={styles.card}
             >
-              Join Room
-            </motion.button>
-          </div>
-        )}
+                <h1 style={styles.logo}>掼蛋</h1>
+                <h2 style={styles.title}>Guan Dan</h2>
 
-        {mode === 'create' && (
-          <div style={styles.form}>
-            <input
-              type="text"
-              placeholder="Your name"
-              value={name}
-              onChange={(e) => set_name(e.target.value)}
-              style={styles.input}
-            />
-            <div style={styles.buttons}>
-              <motion.button
-                whileHover={{ scale: 1.05 }}
-                whileTap={{ scale: 0.95 }}
-                onClick={handle_create}
-                style={styles.button}
-              >
-                Create
-              </motion.button>
-              <motion.button
-                whileHover={{ scale: 1.05 }}
-                whileTap={{ scale: 0.95 }}
-                onClick={() => set_mode('select')}
-                style={{ ...styles.button, backgroundColor: '#6c757d' }}
-              >
-                Back
-              </motion.button>
-            </div>
-          </div>
-        )}
+                {mode === 'select' && (
+                    <div style={styles.buttons}>
+                        <motion.button
+                            whileHover={{ scale: 1.05 }}
+                            whileTap={{ scale: 0.95 }}
+                            onClick={() => set_mode('create')}
+                            style={styles.button}
+                        >
+                            Create Room
+                        </motion.button>
+                        <motion.button
+                            whileHover={{ scale: 1.05 }}
+                            whileTap={{ scale: 0.95 }}
+                            onClick={() => set_mode('join')}
+                            style={{ ...styles.button, backgroundColor: '#28a745' }}
+                        >
+                            Join Room
+                        </motion.button>
+                    </div>
+                )}
 
-        {mode === 'join' && (
-          <div style={styles.form}>
-            <input
-              type="text"
-              placeholder="Your name"
-              value={name}
-              onChange={(e) => set_name(e.target.value)}
-              style={styles.input}
-            />
-            <input
-              type="text"
-              placeholder="Room code"
-              value={join_code}
-              onChange={(e) => set_join_code(e.target.value)}
-              style={styles.input}
-            />
-            <div style={styles.buttons}>
-              <motion.button
-                whileHover={{ scale: 1.05 }}
-                whileTap={{ scale: 0.95 }}
-                onClick={handle_join}
-                style={{ ...styles.button, backgroundColor: '#28a745' }}
-              >
-                Join
-              </motion.button>
-              <motion.button
-                whileHover={{ scale: 1.05 }}
-                whileTap={{ scale: 0.95 }}
-                onClick={() => set_mode('select')}
-                style={{ ...styles.button, backgroundColor: '#6c757d' }}
-              >
-                Back
-              </motion.button>
-            </div>
-          </div>
-        )}
-      </motion.div>
-    </div>
-  )
+                {mode === 'create' && (
+                    <div style={styles.form}>
+                        <input
+                            type="text"
+                            placeholder="Your name"
+                            value={name}
+                            onChange={(e) => set_name(e.target.value)}
+                            style={styles.input}
+                        />
+                        <div style={styles.buttons}>
+                            <motion.button
+                                whileHover={{ scale: 1.05 }}
+                                whileTap={{ scale: 0.95 }}
+                                onClick={handle_create}
+                                style={styles.button}
+                            >
+                                Create
+                            </motion.button>
+                            <motion.button
+                                whileHover={{ scale: 1.05 }}
+                                whileTap={{ scale: 0.95 }}
+                                onClick={() => set_mode('select')}
+                                style={{ ...styles.button, backgroundColor: '#6c757d' }}
+                            >
+                                Back
+                            </motion.button>
+                        </div>
+                    </div>
+                )}
+
+                {mode === 'join' && (
+                    <div style={styles.form}>
+                        <input
+                            type="text"
+                            placeholder="Your name"
+                            value={name}
+                            onChange={(e) => set_name(e.target.value)}
+                            style={styles.input}
+                        />
+                        <input
+                            type="text"
+                            placeholder="Room code"
+                            value={join_code}
+                            onChange={(e) => set_join_code(e.target.value)}
+                            style={styles.input}
+                        />
+                        <div style={styles.buttons}>
+                            <motion.button
+                                whileHover={{ scale: 1.05 }}
+                                whileTap={{ scale: 0.95 }}
+                                onClick={handle_join}
+                                style={{ ...styles.button, backgroundColor: '#28a745' }}
+                            >
+                                Join
+                            </motion.button>
+                            <motion.button
+                                whileHover={{ scale: 1.05 }}
+                                whileTap={{ scale: 0.95 }}
+                                onClick={() => set_mode('select')}
+                                style={{ ...styles.button, backgroundColor: '#6c757d' }}
+                            >
+                                Back
+                            </motion.button>
+                        </div>
+                    </div>
+                )}
+            </motion.div>
+        </div>
+    )
 }
 
 const styles: Record<string, React.CSSProperties> = {
-  container: {
-    display: 'flex',
-    justifyContent: 'center',
-    alignItems: 'center',
-    minHeight: '100vh',
-    backgroundColor: '#1a1a2e',
-  },
-  card: {
-    backgroundColor: '#16213e',
-    padding: 40,
-    borderRadius: 16,
-    textAlign: 'center',
-    boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
-    minWidth: 360,
-  },
-  logo: {
-    fontSize: 64,
-    margin: 0,
-    color: '#fff',
-  },
-  title: {
-    color: '#fff',
-    marginTop: 8,
-    marginBottom: 24,
-  },
-  subtitle: {
-    color: '#aaa',
-    marginBottom: 24,
-  },
-  buttons: {
-    display: 'flex',
-    gap: 12,
-    justifyContent: 'center',
-  },
-  button: {
-    padding: '12px 24px',
-    fontSize: 16,
-    border: 'none',
-    borderRadius: 8,
-    backgroundColor: '#007bff',
-    color: '#fff',
-    cursor: 'pointer',
-  },
-  form: {
-    display: 'flex',
-    flexDirection: 'column',
-    gap: 12,
-  },
-  input: {
-    padding: '12px 16px',
-    fontSize: 16,
-    border: '2px solid #333',
-    borderRadius: 8,
-    backgroundColor: '#0f3460',
-    color: '#fff',
-    outline: 'none',
-  },
-  players_grid: {
-    display: 'grid',
-    gridTemplateColumns: '1fr 1fr',
-    gap: 12,
-    marginBottom: 24,
-  },
-  player_slot: {
-    padding: 16,
-    borderRadius: 8,
-    border: '2px solid',
-  },
-  player_name: {
-    fontWeight: 'bold',
-    color: '#333',
-  },
-  player_team: {
-    fontSize: 12,
-    color: '#666',
-  },
-  empty_slot: {
-    color: '#999',
-  },
-  hint: {
-    color: '#666',
-    fontSize: 12,
-  },
+    container: {
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        minHeight: '100vh',
+        backgroundColor: '#1a1a2e',
+    },
+    card: {
+        backgroundColor: '#16213e',
+        padding: 40,
+        borderRadius: 16,
+        textAlign: 'center',
+        boxShadow: '0 8px 32px rgba(0,0,0,0.3)',
+        minWidth: 360,
+    },
+    logo: {
+        fontSize: 64,
+        margin: 0,
+        color: '#fff',
+    },
+    title: {
+        color: '#fff',
+        marginTop: 8,
+        marginBottom: 24,
+    },
+    subtitle: {
+        color: '#aaa',
+        marginBottom: 24,
+    },
+    buttons: {
+        display: 'flex',
+        gap: 12,
+        justifyContent: 'center',
+    },
+    button: {
+        padding: '12px 24px',
+        fontSize: 16,
+        border: 'none',
+        borderRadius: 8,
+        backgroundColor: '#007bff',
+        color: '#fff',
+        cursor: 'pointer',
+    },
+    form: {
+        display: 'flex',
+        flexDirection: 'column',
+        gap: 12,
+    },
+    input: {
+        padding: '12px 16px',
+        fontSize: 16,
+        border: '2px solid #333',
+        borderRadius: 8,
+        backgroundColor: '#0f3460',
+        color: '#fff',
+        outline: 'none',
+    },
+    players_grid: {
+        display: 'grid',
+        gridTemplateColumns: '1fr 1fr',
+        gap: 12,
+        marginBottom: 24,
+    },
+    player_slot: {
+        padding: 16,
+        borderRadius: 8,
+        border: '2px solid',
+    },
+    player_name: {
+        fontWeight: 'bold',
+        color: '#333',
+    },
+    player_team: {
+        fontSize: 12,
+        color: '#666',
+    },
+    empty_slot: {
+        color: '#999',
+    },
+    hint: {
+        color: '#666',
+        fontSize: 12,
+    },
 }