Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions FEATURE_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Spectator Feature - Validation Summary

## What conversations did you have?

I talked with a few friends who aren't super familiar with crypto, and the feedback was pretty clear - they were hesitant to connect their wallet just to see if the game was even active. One friend said, "I want to see what this is about before I put money in." That made me realize there's a real barrier to entry when you force wallet connection upfront.

I also looked at how other platforms handle this. Chess.com lets anyone watch any game - no sign-up needed. That's partly why they have such a strong community. On the flip side, I noticed some blockchain poker games require wallet connection just to browse, and it feels... gatekeepy? Not a great first impression.

The pattern was consistent: people want to explore and understand before committing, especially when real money (STX) is involved.

## Did you set up a landing page?

Yes! The spectator feature is now accessible in two ways:

1. **Navigation bar** - Added a "Spectate" link in the navbar (alongside Home and Create Game) so it's easily discoverable
2. **Home page banner** - When games are active, there's a live indicator showing "๐Ÿ”ด X games in progress" with a "Watch Live Games" button that takes you to the spectator page

The spectator page itself shows all games - joinable, active, and completed - with the game boards, player addresses, bet amounts, and game status. And crucially, **no wallet connection required**.

## Why do you think this is a good idea to move forward with?

Honestly, it just makes sense from both a technical and UX perspective:

**The Technical Side:**
- All game data is already publicly readable on the blockchain anyway - that's how blockchains work
- I'm just using the existing `getAllGames()` read-only contract function, so minimal code needed
- No security concerns since it's read-only, no authentication involved
- Zero additional infrastructure cost

**The User Side:**
- New users can "try before they buy" - watch a game to understand how it works before betting STX
- It creates social proof - you can see the platform is actually active with real games happening
- It's educational - you learn the game mechanics risk-free
- It lowers friction - no wallet barrier just to explore

**The Competitive Angle:**
Most blockchain games I've seen require wallet connection even to browse. This puts up unnecessary walls. By making spectating frictionless, we're following what works in traditional gaming (think Twitch, Chess.com) while leveraging blockchain's natural transparency.

## What validation steps did you take?

**Quick Testing:**
- Confirmed the page works without wallet connection (tested in incognito mode)
- Verified game data displays accurately and matches on-chain state
- Tested on different devices - works fine on mobile and desktop
- Made sure performance is good even with multiple games loading

**User Feedback:**
Showed it to a few people who aren't crypto-native. The consistent feedback was "Oh, this makes way more sense" compared to being forced to connect a wallet first. One person specifically said they'd be more likely to try playing after watching a game.

**Why It's Low Risk:**
- It's an additive feature - doesn't change existing gameplay at all
- Easy to rollback if needed
- Only uses public data that's already accessible on-chain
- Players can ignore it if they want - it's purely opt-in for spectators

## Moving Forward

This feature feels like a no-brainer. It's technically simple, addresses a real friction point, and aligns with how successful gaming platforms work. The blockchain's transparency is a feature, not a bug - why not use it to build trust and help new users discover the platform?

Next steps are to track some metrics (page views, conversion from spectator โ†’ player) and see if it's actually helping with user acquisition like I think it will.
2 changes: 1 addition & 1 deletion deployments/default.testnet-plan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plan:
transactions:
- contract-publish:
contract-name: tic-tac-toe
expected-sender: ST3P49R8XXQWG69S66MZASYPTTGNDKK0WW32RRJDN
expected-sender: ST12KRGRZ2N2Q5B8HKXHETGRD0JVF282TAAXNM1ZV
cost: 90730
path: contracts/tic-tac-toe.clar
anchor-block-only: true
Expand Down
1 change: 1 addition & 0 deletions frontend/.env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=6b87a3c69cbd8b52055d7aef763148d6
62 changes: 60 additions & 2 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Stacks Tic-Tac-Toe

A decentralized Tic-Tac-Toe game built on the Stacks blockchain. This [Next.js](https://nextjs.org) project allows players to create games, place bets in STX, and compete against each other on-chain.

## Features

- **Create Games**: Start a new game with a custom bet amount in STX
- **Join Games**: Join any available game created by other players
- **Play On-Chain**: All game moves are recorded on the Stacks blockchain
- **Spectate Mode**: Watch any game in progress without connecting a wallet
- **Dual Wallet Integration**: Connect your Stacks wallet via Stacks Connect (browser extension) or WalletConnect (mobile)
- **WalletConnect Support**: Connect mobile wallets securely using WalletConnect protocol (Project ID: 6b87a3c69cbd8b52055d7aef763148d6)
- **Real-time Updates**: View active games, joinable games, and ended games

## WalletConnect Integration

This frontend integrates **WalletConnect SDK v2** for seamless mobile wallet connections:

- **Mobile Wallet Support**: Connect Xverse, Leather, and other Stacks-compatible mobile wallets
- **QR Code Connection**: Scan QR code with your mobile wallet for instant connection
- **Secure Protocol**: End-to-end encrypted communication between app and wallet
- **Session Management**: Persistent sessions with automatic reconnection
- **Multi-Chain Ready**: Built on WalletConnect's latest protocol for future expansion

### How to Connect with WalletConnect

1. Click "Connect Wallet" button
2. Select "Connect with WalletConnect" from the dropdown
3. Scan the QR code with your mobile wallet app
4. Approve the connection request
5. Start playing!

## Getting Started

### Prerequisites

Create a `.env.local` file in the frontend directory with your WalletConnect project ID:

```bash
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id_here
```

You can get a project ID by registering at [WalletConnect Cloud](https://cloud.walletconnect.com/).

### Run Development Server

First, run the development server:

```bash
Expand All @@ -16,7 +58,23 @@ bun dev

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## Project Structure

- `/app` - Next.js app router pages
- `/page.tsx` - Home page with game lists
- `/create/page.tsx` - Create new game page
- `/game/[gameId]/page.tsx` - Individual game page
- `/spectate/page.tsx` - Spectate mode to view all games
- `/components` - Reusable React components
- `navbar.tsx` - Navigation bar with wallet connection
- `game-board.tsx` - Tic-tac-toe board component
- `games-list.tsx` - Display lists of games
- `play-game.tsx` - Game play interface
- `/lib` - Utility functions and contract interactions
- `contract.ts` - Stacks smart contract integration
- `stx-utils.ts` - STX formatting and helper functions
- `/hooks` - Custom React hooks
- `use-stacks.ts` - Stacks wallet integration hook

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

Expand Down
7 changes: 5 additions & 2 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { Navbar } from "@/components/navbar";
import { Providers } from "@/components/providers";

export const metadata: Metadata = {
title: "Tic Tac Toe",
Expand All @@ -15,8 +16,10 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<Navbar />
{children}
<Providers>
<Navbar />
{children}
</Providers>
</body>
</html>
);
Expand Down
19 changes: 19 additions & 0 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { GamesList } from "@/components/games-list";
import { getAllGames } from "@/lib/contract";
import Link from "next/link";

export const dynamic = "force-dynamic";

export default async function Home() {
const games = await getAllGames();
const activeGamesCount = games.filter(
(game) => game.winner === null && game["player-two"] !== null
).length;

return (
<section className="flex flex-col items-center py-20">
Expand All @@ -13,6 +17,21 @@ export default async function Home() {
<span className="text-sm text-gray-500">
Play 1v1 Tic Tac Toe on the Stacks blockchain
</span>

{activeGamesCount > 0 && (
<div className="mt-6 p-4 bg-gray-800 border border-gray-600 rounded-lg inline-block">
<p className="text-sm text-gray-300 mb-3">
๐Ÿ”ด {activeGamesCount} {activeGamesCount === 1 ? "game" : "games"}{" "}
in progress
</p>
<Link
href="/spectate"
className="rounded-lg bg-purple-600 px-4 py-2 text-sm font-medium text-white hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2"
>
Watch Live Games
</Link>
</div>
)}
</div>

<GamesList games={games} />
Expand Down
119 changes: 119 additions & 0 deletions frontend/app/spectate/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { GameBoard } from "@/components/game-board";
import { getAllGames } from "@/lib/contract";
import { abbreviateAddress, explorerAddress, formatStx } from "@/lib/stx-utils";
import Link from "next/link";

export const dynamic = "force-dynamic";

export default async function SpectatePage() {
const games = await getAllGames();

return (
<section className="flex flex-col items-center py-20">
<div className="text-center mb-16">
<h1 className="text-4xl font-bold">Spectate Games</h1>
<span className="text-sm text-gray-500">
Watch any match unfold without connecting a wallet
</span>
</div>

<div className="grid gap-8 w-full max-w-5xl">
{games.length === 0 ? (
<div className="text-center py-12 border rounded-lg">
<p className="text-gray-500 mb-4">No games found yet.</p>
<Link
href="/create"
className="rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Create New Game
</Link>
</div>
) : (
games.map((game) => {
const playerTwo = game["player-two"];
const isJoinable = playerTwo === null;
const isOver = game.winner !== null;
const nextTurn = game["is-player-one-turn"] ? "X" : "O";

return (
<div
key={game.id}
className="border border-gray-700 rounded-lg bg-gray-900 p-6 flex flex-col gap-4"
>
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-semibold">Game #{game.id}</h2>
<div className="text-sm text-gray-400">
Bet: {formatStx(game["bet-amount"])} STX
</div>
</div>

<div className="text-sm px-3 py-1 rounded-full bg-gray-800 border border-gray-600">
{isOver
? "Completed"
: isJoinable
? "Waiting for opponent"
: `Next turn: ${nextTurn}`}
</div>
</div>

<GameBoard
board={game.board}
cellClassName="size-16 text-4xl"
/>

<div className="grid gap-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-gray-500">Player One:</span>
<Link
href={explorerAddress(game["player-one"])}
target="_blank"
className="hover:underline"
>
{abbreviateAddress(game["player-one"])}
</Link>
</div>

<div className="flex items-center justify-between">
<span className="text-gray-500">Player Two:</span>
{playerTwo ? (
<Link
href={explorerAddress(playerTwo)}
target="_blank"
className="hover:underline"
>
{abbreviateAddress(playerTwo)}
</Link>
) : (
<span className="text-gray-300">Not joined</span>
)}
</div>

{game.winner && (
<div className="flex items-center justify-between">
<span className="text-gray-500">Winner:</span>
<Link
href={explorerAddress(game.winner)}
target="_blank"
className="hover:underline"
>
{abbreviateAddress(game.winner)}
</Link>
</div>
)}
</div>

<Link
href={`/game/${game.id}`}
className="self-start text-sm text-blue-400 hover:underline"
>
View full game details
</Link>
</div>
);
})
)}
</div>
</section>
);
}
Loading