# Frontend (/docs/cookbook/vrf-rock-paper-scissors/frontend)







import { Steps, Step } from 'fumadocs-ui/components/steps';
import { Callout } from 'fumadocs-ui/components/callout';
import { Tabs, Tab } from 'fumadocs-ui/components/tabs';

## Frontend Architecture

The frontend provides:

* Wallet connection with RISE Wallet passkeys
* Ticket balance display and purchase
* Game state machine (start → countdown → choice → result)
* Animated transitions between states
* API calls to backend for VRF requests
* Winner determination and display

## Building the UI Components

<Steps>
  <Step>
    ### UI Components

    The Button and Dropdown Menu components were already installed via shadcn/ui in the setup step. They're ready to use from `@/components/ui`.
  </Step>

  <Step>
    ### Create Navbar Component

    Create `src/components/Navbar.tsx`:

    ```typescript
    "use client"

    import { useAccount, useBalance, useDisconnect, useReadContract } from 'wagmi'
    import { Button } from './ui/button'
    import { RPS_ADDRESS, ABI } from '@/constants'
    import { useState } from 'react'
    import { BuyTicketsModal } from './BuyTicketsModal'

    export function Navbar() {
      const { address, isConnected } = useAccount()
      const { disconnect } = useDisconnect()
      const [showBuyModal, setShowBuyModal] = useState(false)

      const { data: balance } = useBalance({
        address: address,
      })

      const { data: tickets, refetch: refetchTickets } = useReadContract({
        address: RPS_ADDRESS,
        abi: ABI,
        functionName: 'ticketBalance',
        args: address ? [address] : undefined,
      })

      if (!isConnected) return null

      return (
        <>
          <nav className="border-b">
            <div className="container mx-auto px-4 py-4 flex items-center justify-between">
              <h1 className="text-xl font-bold">VRF Rock Paper Scissors</h1>

              <div className="flex items-center gap-4">
                <div className="text-sm">
                  <div>Balance: {balance?.formatted.slice(0, 6)} ETH</div>
                  <div>Tickets: {tickets?.toString() || '0'}</div>
                </div>

                <Button onClick={() => setShowBuyModal(true)} size="lg">
                  Buy Tickets
                </Button>

                <div className="text-sm">
                  {address?.slice(0, 6)}...{address?.slice(-4)}
                </div>

                <Button onClick={() => disconnect()} variant="outline">
                  Disconnect
                </Button>
              </div>
            </div>
          </nav>

          <BuyTicketsModal
            open={showBuyModal}
            onClose={() => setShowBuyModal(false)}
            onSuccess={refetchTickets}
          />
        </>
      )
    }
    ```
  </Step>

  <Step>
    ### Create Ticket Purchase Modal

    Create `src/components/BuyTicketsModal.tsx`:

    ```typescript
    "use client"

    import { useState } from 'react'
    import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
    import { RPS_ADDRESS, ABI } from '@/constants'
    import { Button } from './ui/button'
    import { parseEther } from 'viem'

    interface BuyTicketsModalProps {
      open: boolean
      onClose: () => void
      onSuccess: () => void
    }

    export function BuyTicketsModal({ open, onClose, onSuccess }: BuyTicketsModalProps) {
      const [numTickets, setNumTickets] = useState(1)
      const { writeContract, data: hash } = useWriteContract()

      const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
        hash,
      })

      const handleBuy = async () => {
        try {
          writeContract({
            address: RPS_ADDRESS,
            abi: ABI,
            functionName: 'buyTickets',
            args: [BigInt(numTickets)],
            value: parseEther((0.001 * numTickets).toString()),
          })
        } catch (error) {
          console.error('Error buying tickets:', error)
          alert('Failed to buy tickets')
        }
      }

      if (isSuccess) {
        onSuccess()
        onClose()
      }

      if (!open) return null

      return (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
          <div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
            <h2 className="text-2xl font-bold mb-4">Buy Tickets</h2>

            <div className="mb-4">
              <label className="block text-sm font-medium mb-2">
                Number of Tickets (0.001 ETH each)
              </label>
              <input
                type="number"
                min="1"
                max="10"
                value={numTickets}
                onChange={(e) => setNumTickets(parseInt(e.target.value))}
                className="w-full border rounded px-3 py-2"
              />
            </div>

            <div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
              Total: {(0.001 * numTickets).toFixed(3)} ETH
            </div>

            <div className="flex gap-2">
              <Button
                onClick={handleBuy}
                disabled={isConfirming}
                className="flex-1"
              >
                {isConfirming ? 'Buying...' : 'Buy Tickets'}
              </Button>
              <Button
                onClick={onClose}
                variant="outline"
                disabled={isConfirming}
              >
                Cancel
              </Button>
            </div>
          </div>
        </div>
      )
    }
    ```
  </Step>

  <Step>
    ### Create Main Game Page

    Create `src/app/page.tsx`:

    ```typescript
    "use client"

    import { useState, useEffect } from 'react'
    import { useAccount, useConnect, useReadContract } from 'wagmi'
    import { Button } from '@/components/ui/button'
    import { Navbar } from '@/components/Navbar'
    import { RPS_ADDRESS, ABI } from '@/constants'

    type GameState = "start" | "countdown" | "choice" | "ai-choosing" | "result"
    type Choice = "rock" | "paper" | "scissors" | null

    export default function Home() {
      const { address, isConnected } = useAccount()
      const { connect, connectors } = useConnect()

      const [gameState, setGameState] = useState<GameState>("start")
      const [playerChoice, setPlayerChoice] = useState<Choice>(null)
      const [aiChoice, setAiChoice] = useState<Choice>(null)
      const [countdown, setCountdown] = useState(0)
      const [gameResult, setGameResult] = useState<"win" | "lose" | "tie" | null>(null)

      const { data: tickets, refetch: refetchBalance } = useReadContract({
        address: RPS_ADDRESS,
        abi: ABI,
        functionName: 'ticketBalance',
        args: address ? [address] : undefined,
      })

      // Countdown timer
      useEffect(() => {
        if (gameState === 'countdown' && countdown > 0) {
          const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
          return () => clearTimeout(timer)
        } else if (gameState === 'countdown' && countdown === 0) {
          setGameState('choice')
        }
      }, [gameState, countdown])

      const startGame = () => {
        setGameState('countdown')
        setCountdown(3)
        setPlayerChoice(null)
        setAiChoice(null)
        setWinner(null)
      }

      const makeChoice = async (choice: Choice) => {
        setPlayerChoice(choice)
        setGameState('ai-choosing')

        try {
          const response = await fetch('/api', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ address, choice }),
          })

          const data = await response.json()

          if (!response.ok) {
            alert(data.error || 'Failed to play game')
            setGameState('start')
            return
          }

          // Convert result to choice: 0=rock, 1=paper, 2=scissors
          const aiChoiceMap: Choice[] = ["rock", "paper", "scissors"]
          const aiResult = aiChoiceMap[data.result]
          setAiChoice(aiResult)

          // Determine win/lose/tie
          const result = determineWinner(choice, aiResult)
          setGameResult(result)

          setGameState("result")
          refetchBalance()

        } catch (error) {
          console.error('Error:', error)
          alert('Failed to play game')
          setGameState('start')
        }
      }

      const determineWinner = (player: Choice, ai: Choice): "win" | "lose" | "tie" => {
        if (player === ai) return "tie"

        const winConditions = {
          rock: "scissors",
          paper: "rock",
          scissors: "paper"
        }

        return winConditions[player as keyof typeof winConditions] === ai ? "win" : "lose"
      }

      if (!isConnected) {
        return (
          <main className="min-h-screen flex items-center justify-center">
            <div className="text-center">
              <h1 className="text-4xl font-bold mb-8">VRF Rock Paper Scissors</h1>
              <p className="mb-8 text-gray-600">Connect with RISE Wallet to play</p>
              <Button
                onClick={() => connect({ connector: connectors[0] })}
                size="lg"
              >
                Connect with Passkey
              </Button>
            </div>
          </main>
        )
      }

      return (
        <>
          <Navbar />
          <main className="min-h-screen flex items-center justify-center p-4">
            <div className="max-w-2xl w-full">

              {/* Start State */}
              {gameState === 'start' && (
                <div className="text-center">
                  <h1 className="text-4xl font-bold mb-4">Rock Paper Scissors</h1>
                  <p className="mb-8 text-gray-600">
                    Play against AI powered by RISE VRF
                  </p>
                  <p className="mb-8">
                    Tickets: {tickets?.toString() || '0'}
                  </p>
                  <Button
                    onClick={startGame}
                    size="lg"
                    disabled={!tickets || tickets === BigInt(0)}
                  >
                    {tickets && tickets > BigInt(0) ? 'Start Game' : 'Buy Tickets to Play'}
                  </Button>
                </div>
              )}

              {/* Countdown State */}
              {gameState === 'countdown' && (
                <div className="text-center">
                  <h2 className="text-6xl font-bold mb-8">
                    {countdown === 3 ? '🪨' : countdown === 2 ? '📄' : '✂️'}
                  </h2>
                  <p className="text-2xl">Get ready...</p>
                </div>
              )}

              {/* Choice State */}
              {gameState === 'choice' && (
                <div className="text-center">
                  <h2 className="text-3xl font-bold mb-8">Make your choice!</h2>
                  <div className="flex gap-4 justify-center">
                    <Button
                      onClick={() => makeChoice('rock')}
                      size="lg"
                      className="text-6xl p-8"
                    >
                      🪨
                    </Button>
                    <Button
                      onClick={() => makeChoice('paper')}
                      size="lg"
                      className="text-6xl p-8"
                    >
                      📄
                    </Button>
                    <Button
                      onClick={() => makeChoice('scissors')}
                      size="lg"
                      className="text-6xl p-8"
                    >
                      ✂️
                    </Button>
                  </div>
                </div>
              )}

              {/* AI Choosing State */}
              {gameState === 'ai-choosing' && (
                <div className="text-center">
                  <h2 className="text-3xl font-bold mb-8">AI is choosing...</h2>
                  <div className="animate-pulse text-6xl">🤖</div>
                </div>
              )}

              {/* Result State */}
              {gameState === 'result' && (
                <div className="text-center">
                  <h2 className="text-3xl font-bold mb-8">
                    {winner === 'player' ? '🎉 You Win!' : winner === 'ai' ? '😢 You Lose!' : '🤝 Tie!'}
                  </h2>
                  <div className="flex gap-8 justify-center mb-8 text-6xl">
                    <div>
                      <p className="text-sm mb-2">You</p>
                      {playerChoice === 'rock' ? '🪨' : playerChoice === 'paper' ? '📄' : '✂️'}
                    </div>
                    <div className="text-4xl self-center">VS</div>
                    <div>
                      <p className="text-sm mb-2">AI</p>
                      {aiChoice === 0 ? '🪨' : aiChoice === 1 ? '📄' : '✂️'}
                    </div>
                  </div>
                  {winner === 'player' && (
                    <p className="mb-4 text-green-600 font-bold">You won 0.002 ETH!</p>
                  )}
                  <Button onClick={() => setGameState('start')} size="lg">
                    Play Again
                  </Button>
                </div>
              )}

            </div>
          </main>
        </>
      )
    }
    ```
  </Step>
</Steps>

## Understanding the Game Flow

### State Machine

The game follows this state progression:

```
start → countdown → choice → ai-choosing → result → start
```

**start**: Initial state, shows "Start Game" button

**countdown**: Rock-Paper-Scissors animation with emoji transitions (🪨 → 📄 → ✂️)

**choice**: Player selects rock, paper, or scissors

**ai-choosing**: Loading state while backend calls VRF API

**result**: Shows winner, both choices, and reward info

### API Integration

```typescript
const response = await fetch('/api', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ address, choice }),
})

const data = await response.json()
setAiChoice(data.result)  // 0=rock, 1=paper, 2=scissors
```

The frontend calls the backend API we built earlier. The backend handles all VRF logic and returns the result.

### Ticket Balance

```typescript
const { data: tickets, refetch: refetchBalance } = useReadContract({
  address: RPS_ADDRESS,
  abi: ABI,
  functionName: 'ticketBalance',
  args: address ? [address] : undefined,
})
```

We use wagmi's `useReadContract` to read the player's ticket balance from the smart contract. After each game, we refetch to show updated balance.

### Winner Logic

```typescript
const determineWinner = (player: Choice, ai: Choice): "win" | "lose" | "tie" => {
  if (player === ai) return "tie"

  const winConditions = {
    rock: "scissors",
    paper: "rock",
    scissors: "paper"
  }

  return winConditions[player as keyof typeof winConditions] === ai ? "win" : "lose"
}
```

Standard RPS logic: rock beats scissors, paper beats rock, scissors beats paper. Implemented on frontend for immediate UI feedback.

## Styling

Update `src/app/globals.css`:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 0 0% 3.9%;
    --primary: 0 0% 9%;
    --primary-foreground: 0 0% 98%;
  }

  .dark {
    --background: 0 0% 3.9%;
    --foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 0 0% 9%;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}
```

## Testing the Complete App

<Steps>
  <Step>
    ### Start Development Server

    <Tabs items={['npm', 'yarn', 'pnpm', 'bun']} groupId="package-manager">
      <Tab value="npm">
        ```bash
        npm run dev
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn dev
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm dev
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bun dev
        ```
      </Tab>
    </Tabs>

    Visit `http://localhost:3000`

        <img alt="VRF Rock-Paper-Scissors Landing Page" src={__img0} placeholder="blur" />
  </Step>

  <Step>
    ### Test Flow

    1. Click "Connect with Passkey"
    2. Authenticate with your passkey (FaceID/TouchID)
    3. Click "Buy Tickets"
    4. Purchase 1-10 tickets (0.001 ETH each)
    5. Click "Start Game"
    6. Watch the Rock-Paper-Scissors countdown (🪨 → 📄 → ✂️)
    7. Choose your move
    8. See VRF result quickly
    9. If you win, receive 0.002 ETH automatically

        <img alt="Active Gameplay - Choose Your Move" src={__img1} placeholder="blur" />
  </Step>

  <Step>
    ### Game Result

    After making your choice, the VRF result appears within milliseconds showing the AI's choice and whether you won, lost, or tied.

        <img alt="Game Result Screen" src={__img2} placeholder="blur" />
  </Step>

  <Step>
    ### Verify Everything Works

    Check that:

    * Wallet connects smoothly
    * Ticket balance updates after purchase
    * Game plays without errors
    * VRF result arrives quickly
    * Winner is displayed correctly
    * Rewards arrive for wins
    * Can play multiple games in a row
  </Step>
</Steps>

## Next Steps

Congratulations! You've built a complete VRF-powered game. Here are some ideas to extend it:

* Add leaderboard with top players
* Implement streak tracking
* Add multiplayer mode (PvP instead of PvE)
* Create NFT rewards for winning streaks
* Add sound effects and animations
* Implement session keys for popup-free gameplay

## Common Issues

**"Transaction reverted"**: Player has no tickets. Buy tickets first.

**"VRF request timed out"**: Network issue or VRF coordinator down. Retry.

**Tickets not updating**: Call `refetchBalance()` after purchases/games.

**Wallet won't connect**: Make sure you're on RISE Testnet and using a compatible browser (Chrome/Brave).

<Callout type="info">
  Need help? Check the [complete source code](https://github.com/rise-cookbook/vrf-rps) or ask in the [RISE Discord](https://discord.gg/risechain).
</Callout>
