> For the complete documentation index, see [llms.txt](https://docs.seismic.systems/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.seismic.systems/tutorials/understanding-the-clown-beatdown-contract/building-the-frontend/chapter-3-game-ui-components.md).

# Ch 3: Game UI Components

In this chapter, you'll build the game interface — the clown sprite with punch animations, action buttons, and the entry screen. *Estimated time: \~15 minutes*

### ShowClown: Animated clown sprite

The clown sprite changes appearance based on how many times it's been hit. Create `src/components/game/ShowClown.tsx`:

```typescript
import { motion, useAnimation } from 'framer-motion'
import { useEffect, useMemo } from 'react'

import { Box } from '@mui/material'

type ClownProps = {
  isKO: boolean
  isShakingAnimation: boolean
  isHittingAnimation: boolean
  punchCount: number
}

const ShowClown: React.FC<ClownProps> = ({
  isKO,
  isShakingAnimation,
  isHittingAnimation,
  punchCount,
}) => {
  const controls = useAnimation()

  useEffect(() => {
    if (isShakingAnimation) {
      controls.start({
        rotate: [0, -5, 5, -5, 5, 0],
        transition: { duration: 0.5 },
      })
    } else if (isHittingAnimation) {
      controls.start({
        scale: [1, 0.9, 1.1, 1],
        transition: { duration: 0.3 },
      })
    }
  }, [isShakingAnimation, isHittingAnimation, controls])

  // Select the appropriate clown image based on punch count and KO state
  // Using useMemo to prevent recalculating on every render
  const clownImage = useMemo(() => {
    // If shaking, show the shaking clown image
    if (isShakingAnimation) {
      return '/clown_shaking.png'
    }

    if (isKO) {
      return '/clownko.png'
    }

    let imagePath
    switch (punchCount) {
      case 0:
        imagePath = '/clown1.png'
        break
      case 1:
        imagePath = '/clown2.png'
        break
      case 2:
      case 3:
        imagePath = '/clown3.png'
        break
      default:
        imagePath = '/clown1.png'
    }

    return imagePath
  }, [isKO, isShakingAnimation, punchCount])

  return (
    <Box
      sx={{
        width: { xs: '70%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
        height: { xs: '100%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <div className="relative">
        <motion.div animate={controls} className="relative">
          <img
            src={clownImage}
            alt="Clown"
            style={{
              maxWidth: '100%',
              height: 'auto',
              objectFit: 'contain',
            }}
          />
        </motion.div>
      </div>
    </Box>
  )
}

export default ShowClown
```

The sprite progression creates visual feedback as the clown takes damage:

* **0 hits** — `clown1.png` (full health)
* **1 hit** — `clown2.png` (damaged)
* **2–3 hits** — `clown3.png` (heavily damaged)
* **KO** — `clownko.png` (knocked out)
* **Shaking** — `clown_shaking.png` (mid-animation)

Framer Motion's `useAnimation` hook controls two animations: a **shake** (rotation) when the clown gets hit, and a **scale punch** effect for impact feedback.

### ButtonContainer: Action buttons

The button container renders the game's action buttons — hit, rob, and reset. Create `src/components/game/ButtonContainer.tsx`:

```typescript
import { useState } from 'react'

import { Box, type SxProps, type Theme } from '@mui/material'

type ButtonContainerProps = {
  clownStamina: number | null
  isHitting: boolean
  isResetting: boolean
  isRobbing: boolean
  handleHit: () => void
  handleReset: () => void
  handleRob: () => void
  position?: 'left' | 'right' | 'mobile'
}

type ActionButtonProps = {
  onClick: () => void
  active: boolean
  src: string
  alt: string
  className: string
  sx?: SxProps<Theme>
}

const ActionButton = ({
  onClick,
  active,
  src,
  alt,
  className,
  sx,
}: ActionButtonProps) => (
  <Box
    onClick={onClick}
    component="div"
    sx={{
      cursor: active ? 'default' : 'pointer',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      ...sx,
    }}
  >
    <img
      src={src}
      alt={alt}
      className={className}
      style={{ width: '100%', height: '100%', objectFit: 'contain' }}
    />
  </Box>
)

export default function ButtonContainer({
  clownStamina,
  isHitting,
  isResetting,
  isRobbing,
  handleHit,
  handleReset,
  handleRob,
  position = 'mobile',
}: ButtonContainerProps) {
  const [showRobActive, setShowRobActive] = useState(false)
  const [showResetActive, setShowResetActive] = useState(false)

  const handleRobClick = () => {
    if (!isRobbing) {
      setShowRobActive(true)
      setTimeout(() => {
        setShowRobActive(false)
        handleRob()
      }, 200)
    }
  }

  const handleResetClick = () => {
    if (!isResetting) {
      setShowResetActive(true)
      setTimeout(() => {
        setShowResetActive(false)
        handleReset()
      }, 200)
    }
  }

  const isStanding = clownStamina !== null && clownStamina > 0

  const robBtn = {
    onClick: handleRobClick,
    active: isRobbing,
    src: showRobActive ? '/rob_active.png' : '/rob_btn.png',
    alt: 'Rob',
    className: 'look-btn',
  }

  const hitBtn = {
    onClick: handleHit,
    active: isHitting,
    src: isHitting ? '/punch_active.png' : '/punch_btn.png',
    alt: 'Punch',
    className: 'punch-btn',
  }

  const resetBtn = {
    onClick: handleResetClick,
    active: isResetting,
    src: showResetActive ? '/reset_active.png' : '/reset_btn.png',
    alt: 'Reset',
    className: 'reset-btn',
  }

  const rightBtn = isStanding ? hitBtn : resetBtn

  if (position === 'left') {
    return (
      <Box
        sx={{
          width: { lg: '100%' },
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'flex-end',
          pr: { lg: 4, xl: 6 },
        }}
      >
        <ActionButton
          {...robBtn}
          sx={{ width: '20rem', marginRight: 6, height: '20rem' }}
        />
      </Box>
    )
  }

  if (position === 'right') {
    return (
      <Box
        sx={{
          width: { lg: '100%' },
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'flex-start',
          pl: { lg: 4, xl: 6 },
        }}
      >
        <ActionButton
          {...rightBtn}
          sx={
            isStanding
              ? {
                  marginLeft: { xs: 0, lg: 8 },
                  height: { lg: '18rem' },
                }
              : { width: '20rem', marginLeft: '3rem', height: '18rem' }
          }
        />
      </Box>
    )
  }

  // Mobile layout — both buttons side by side
  const MOBILE_SIZE = {
    xs: '12rem',
    sm: '20rem',
    md: '20rem',
    lg: '30rem',
    xl: '30rem',
  }

  return (
    <Box
      sx={{
        width: '100%',
        height: '100%',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: { xs: 0, sm: 0, md: 0, lg: 70, xl: 70 },
        marginRight: { xs: 0, sm: 4, md: 4, lg: 6, xl: 0 },
        marginLeft: { xs: 0, sm: 4, md: 4, lg: 6, xl: 0 },
      }}
    >
      <ActionButton
        {...robBtn}
        sx={{ height: MOBILE_SIZE, width: MOBILE_SIZE }}
      />
      <ActionButton
        {...rightBtn}
        sx={
          isStanding
            ? {
                marginRight: { xs: 0, sm: 4, md: 0, lg: 0, xl: 0 },
                height: {
                  xs: '10rem',
                  sm: '18rem',
                  md: '20rem',
                  lg: '30rem',
                  xl: '30rem',
                },
                width: {
                  xs: '12rem',
                  sm: '14rem',
                  md: '28rem',
                  lg: '30rem',
                  xl: '30rem',
                },
              }
            : { height: MOBILE_SIZE, width: MOBILE_SIZE }
        }
      />
    </Box>
  )
}
```

The button layout adapts based on game state and screen size:

* **Desktop** — rob button on the left, hit/reset on the right
* **Mobile** — both buttons side by side below the clown
* **Clown standing** — show Hit button (right)
* **Clown KO** — show Reset button (right), Rob button now callable (left)

### ClownPuncher: Main game component

This component ties everything together. Create `src/components/game/ClownPuncher.tsx`:

```typescript
'use client'

import { useEffect, useRef, useState } from 'react'

import { useGameActions } from '@/hooks/useGameActions'
import {
  Backdrop,
  Box,
  CircularProgress,
  Container,
  Fade,
  Typography,
} from '@mui/material'

import { useAuth } from '../chain/WalletConnectButton'
import ButtonContainer from './ButtonContainer'
import EntryScreen from './EntryScreen'
import ShowClown from './ShowClown'

const ClownPuncher: React.FC = () => {
  const { isAuthenticated } = useAuth()
  const [showGame, setShowGame] = useState(false)
  const [showSecretSplash, setShowSecretSplash] = useState(false)
  const [showRobRefused, setShowRobRefused] = useState(false)
  const prevRoundIdRef = useRef<number | null>(null)
  const {
    loaded,
    currentRoundId,
    clownStamina,
    isHitting,
    isResetting,
    isRobbing,
    robResult,
    punchCount,
    fetchGameRounds,
    resetGameState,
    handleHit,
    handleReset,
    handleRob,
  } = useGameActions()

  useEffect(() => {
    // Only fetch data if authenticated and game is shown
    if (isAuthenticated && showGame) {
      fetchGameRounds()
    }
  }, [fetchGameRounds, isAuthenticated, showGame])

  useEffect(() => {
    // Only reset game state when first showing the game or when the round actually changes
    if (
      showGame &&
      (prevRoundIdRef.current === null ||
        (currentRoundId !== null && prevRoundIdRef.current !== currentRoundId))
    ) {
      console.log(
        'Round changed from',
        prevRoundIdRef.current,
        'to',
        currentRoundId,
        '- resetting game state'
      )
      resetGameState()
    }
    // Update the ref to the current round ID
    prevRoundIdRef.current = currentRoundId
  }, [currentRoundId, resetGameState, showGame])

  // Show splash screen when lookResult changes to a non-null value
  useEffect(() => {
    if (robResult !== null) {
      setShowSecretSplash(true)
    }
  }, [robResult])

  // If not showing the game yet, show entry screen
  if (!showGame) {
    return <EntryScreen onEnter={() => setShowGame(true)} />
  }

  const onRob = () => {
    if (clownStamina !== null && clownStamina > 0) {
      setShowRobRefused(true)
      return
    }
    handleRob()
  }

  const buttonProps = {
    clownStamina,
    isHitting,
    isResetting,
    isRobbing,
    handleHit,
    handleReset,
    handleRob: onRob,
  } as const

  return (
    <Container
      sx={{
        height: '100dvh',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'start',
        px: 4,
      }}
    >
      <Box
        sx={{
          mt: { xs: 3, sm: 3, md: 5, lg: 4, xl: 10 },
          height: {
            xs: '30dvh',
          },
          mb: 2,
          display: 'flex',
          justifyContent: 'center',
        }}
      >
        <img
          src="/cblogo.png"
          alt="Clown Beatdown Logo"
          className="clown-logo"
        />
      </Box>

      {/* Splash Screen — secret revealed or rob refused */}
      <Backdrop
        sx={{
          color: '#fff',
          zIndex: (theme) => theme.zIndex.drawer + 1,
          backgroundColor: 'rgba(0, 0, 0, 0.85)',
        }}
        open={(showSecretSplash && robResult !== null) || showRobRefused}
        onClick={() => {
          setShowSecretSplash(false)
          setShowRobRefused(false)
        }}
      >
        <Fade in={(showSecretSplash && robResult !== null) || showRobRefused}>
          <Box
            sx={{
              backgroundColor: 'background.paper',
              borderRadius: 4,
              p: 5,
              textAlign: 'center',
              maxWidth: '90%',
              boxShadow: 24,
            }}
          >
            {showRobRefused ? (
              <>
                <Typography
                  variant="h4"
                  fontWeight="bold"
                  color="white"
                  gutterBottom
                >
                  NOT SO FAST!
                </Typography>
                <Typography variant="h6" color="white" gutterBottom>
                  The clown isn't giving up that easily.
                </Typography>
                <Typography
                  variant="body1"
                  color="text.secondary"
                  sx={{ mt: 2 }}
                >
                  Knock him out first!
                </Typography>
              </>
            ) : (
              <>
                <Typography
                  variant="h4"
                  fontWeight="bold"
                  color="white"
                  gutterBottom
                >
                  SECRET REVEALED!
                </Typography>
                <Typography
                  variant="h1"
                  fontWeight="bold"
                  color="white"
                  gutterBottom
                >
                  {robResult}
                </Typography>
              </>
            )}
            <Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
              (Click anywhere to close)
            </Typography>
          </Box>
        </Fade>
      </Backdrop>

      {loaded ? (
        <Box
          sx={{
            display: 'flex',
            flexDirection: { xs: 'column', lg: 'row' },
            justifyContent: { lg: 'space-between' },
            alignItems: 'center',
            width: '100%',
            position: 'relative',
            height: { lg: '500px', xl: '600px' },
            my: { xs: 0, md: 5, lg: 0, xl: 1 },
          }}
        >
          {/* Desktop: left buttons */}
          <Box sx={{ display: { xs: 'none', lg: 'flex' } }}>
            <ButtonContainer {...buttonProps} position="left" />
          </Box>

          {/* Clown — rendered once, responsive positioning */}
          <Box
            className="clown-container"
            sx={{
              display: 'flex',
              justifyContent: 'center',
              alignItems: 'center',
              position: { lg: 'absolute' },
              left: { lg: '50%' },
              transform: { lg: 'translateX(-50%)' },
              zIndex: 2,
              width: { lg: '50%', xl: '40%' },
              maxHeight: { xs: '35dvh', md: '30dvh', lg: 'none' },
            }}
          >
            <ShowClown
              isKO={clownStamina === 0}
              isShakingAnimation={false}
              isHittingAnimation={isHitting}
              punchCount={punchCount}
            />
          </Box>

          {/* Desktop: right buttons */}
          <Box sx={{ display: { xs: 'none', lg: 'flex' } }}>
            <ButtonContainer {...buttonProps} position="right" />
          </Box>

          {/* Mobile: all buttons below clown */}
          <Box
            sx={{
              display: { xs: 'flex', lg: 'none' },
              width: '100%',
              justifyContent: 'center',
              alignItems: 'center',
              marginBottom: { xs: 3, md: 5 },
            }}
          >
            <ButtonContainer {...buttonProps} position="mobile" />
          </Box>
        </Box>
      ) : (
        <CircularProgress size={32} />
      )}
    </Container>
  )
}

export default ClownPuncher
```

### EntryScreen: Wallet connection

The entry screen prompts the user to connect their wallet before playing. Create `src/components/game/EntryScreen.tsx`:

```typescript
import React, { useEffect, useState } from 'react'

import { Box, Container } from '@mui/material'

import { useAuth } from '../chain/WalletConnectButton'

type EntryScreenProps = {
  onEnter: () => void
}

const EntryScreen: React.FC<EntryScreenProps> = ({ onEnter }) => {
  const { isAuthenticated, isLoading, openConnectModal } = useAuth()
  const [isAnimating, setIsAnimating] = useState(false)

  // Automatically enter when user becomes authenticated
  useEffect(() => {
    if (isAuthenticated) {
      setIsAnimating(true)
      setTimeout(() => {
        setIsAnimating(false)
        onEnter()
      }, 500)
    }
  }, [isAuthenticated, onEnter])

  const handleLogoClick = () => {
    setIsAnimating(true)

    // If not authenticated, open wallet connect modal
    if (!isAuthenticated) {
      setTimeout(() => {
        setIsAnimating(false)
        openConnectModal()
      }, 300)
    }
  }

  return (
    <Container
      sx={{
        height: '100dvh',
        width: '100dvw',
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        position: 'relative',
      }}
    >
      <Box
        sx={{
          display: 'flex',
          justifyContent: 'center',
          flexDirection: 'column',
          cursor: 'pointer',
          transform: isAnimating ? 'scale(0.95)' : 'scale(1)',
          transition: 'transform 0.2s ease-in-out',
        }}
        onClick={handleLogoClick}
      >
        <img
          src="/cblogo.png"
          alt="Clown Beatdown Logo"
          style={{ maxWidth: '100%', height: 'auto' }}
          className="clown-image"
        />
        <Box
          sx={{
            mt: 6,
            color: 'black',
            fontSize: '1.25rem',
            textAlign: 'center',
            opacity: 0.8,
            fontFamily: 'monospace',
            border: '1px solid black',
            borderRadius: '10px',
            padding: '10px',
            backgroundColor: 'var(--midColor)',
          }}
        >
          {isLoading ? '...Loading...' : 'CLICK TO CONNECT'}
        </Box>
      </Box>
    </Container>
  )
}

export default EntryScreen
```

Clicking the logo opens the RainbowKit wallet connection modal. Once authenticated, the `onEnter` callback fires and the `ClownPuncher` component takes over.

### WalletConnectButton: Auth context

Create the auth context and wallet button used throughout the app. Create `src/components/chain/WalletConnectButton.tsx`:

```typescript
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useAccount } from 'wagmi'

import { ConnectButton } from '@rainbow-me/rainbowkit'
import { useConnectModal } from '@rainbow-me/rainbowkit'

// Create authentication context
type AuthContextType = {
  isAuthenticated: boolean
  isLoading: boolean
  openConnectModal: () => void
  accountName?: string
}

const AuthContext = createContext<AuthContextType>({
  isAuthenticated: false,
  isLoading: true,
  openConnectModal: () => {},
})

export const useAuth = () => useContext(AuthContext)

// Wallet icon component using SVG for better quality
const WalletIcon = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    viewBox="0 0 24 24"
    fill="currentColor"
    className="w-5 h-5"
  >
    <path d="M2.273 5.625A4.483 4.483 0 0 1 5.25 4.5h13.5c1.141 0 2.183.425 2.977 1.125A3 3 0 0 0 18.75 3H5.25a3 3 0 0 0-2.977 2.625ZM2.273 8.625A4.483 4.483 0 0 1 5.25 7.5h13.5c1.141 0 2.183.425 2.977 1.125A3 3 0 0 0 18.75 6H5.25a3 3 0 0 0-2.977 2.625ZM5.25 9a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h13.5a3 3 0 0 0 3-3v-6a3 3 0 0 0-3-3H15a.75.75 0 0 0-.75.75 2.25 2.25 0 0 1-4.5 0A.75.75 0 0 0 9 9H5.25Z" />
  </svg>
)

const WalletButton: React.FC<
  React.PropsWithChildren<
    { onClick: () => void } & React.HTMLAttributes<HTMLButtonElement>
  >
> = ({ children, onClick, ...props }) => {
  return (
    <button onClick={onClick} className="" {...props}>
      {children}
    </button>
  )
}

export const AuthProvider: React.FC<React.PropsWithChildren> = ({
  children,
}) => {
  const { openConnectModal } = useConnectModal() || {
    openConnectModal: () => {},
  }
  const { address, isConnecting, isConnected, isDisconnected } = useAccount()
  const [authState, setAuthState] = useState<AuthContextType>({
    isAuthenticated: false,
    isLoading: true,
    openConnectModal: openConnectModal || (() => {}),
  })

  useEffect(() => {
    setAuthState({
      isAuthenticated: isConnected,
      isLoading: isConnecting,
      openConnectModal: openConnectModal || (() => {}),
      accountName: address
        ? `${address.slice(0, 6)}...${address.slice(-4)}`
        : undefined,
    })
  }, [isConnected, isConnecting, isDisconnected, address, openConnectModal])

  return (
    <AuthContext.Provider value={authState}>{children}</AuthContext.Provider>
  )
}

const WalletConnectButton = () => {
  return (
    <ConnectButton.Custom>
      {({
        account,
        openConnectModal,
        chain,
        openAccountModal,
        openChainModal,
        mounted,
        authenticationStatus,
      }) => {
        if (!mounted || authenticationStatus === 'loading') {
          return <></>
        }
        if (!account || authenticationStatus === 'unauthenticated') {
          return (
            <WalletButton onClick={openConnectModal}>
              <span className="md:inline hidden">CONNECT WALLET</span>
              <span className="md:hidden">
                <WalletIcon />
              </span>
            </WalletButton>
          )
        }
        if (chain?.unsupported) {
          return (
            <WalletButton onClick={openChainModal}>
              <span className="md:inline hidden">Unsupported chain</span>
              <span className="md:hidden">
                <WalletIcon />
              </span>
            </WalletButton>
          )
        }
        return (
          <WalletButton onClick={openAccountModal}>
            <span className="">
              <WalletIcon />
            </span>
          </WalletButton>
        )
      }}
    </ConnectButton.Custom>
  )
}

export default WalletConnectButton
```

### Running the frontend

Start the frontend dev server:

```bash
cd packages/web
bun dev
```

Make sure `sanvil` is running and the contract is deployed (see [Deploying](/tutorials/understanding-the-clown-beatdown-contract/writing-the-contract/deploying.md)). Open `http://localhost:5173` in your browser, connect your wallet, and start punching the clown!

### Game flow recap

1. **Connect wallet** — RainbowKit modal, ShieldedWalletProvider derives shielded keys
2. **Hit the clown** — `twrite.hit()` sends a shielded transaction, stamina decrements
3. **Clown KO** — stamina reaches 0, sprite changes to `clownko.png`
4. **Rob a secret** — `read.rob()` performs a signed read, secret is decrypted and displayed
5. **Reset** — `twrite.reset()` restores stamina and picks a new random secret for the next round

Congratulations! You've built a complete Seismic dApp — from smart contract to CLI to web frontend.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.seismic.systems/tutorials/understanding-the-clown-beatdown-contract/building-the-frontend/chapter-3-game-ui-components.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
