Skip to content

Commit b186960

Browse files
committed
add Hangman game
1 parent 805f8eb commit b186960

32 files changed

+80220
-0
lines changed
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"]
3+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
37+
38+
errors.json
39+
RecordOnchainSteps.md
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# You can remove this file if you don't want to use Yarn package manager.
2+
nodeLinker: node-modules
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-onchain`]().
2+
3+
## Onchain Hangman
4+
5+
# MIT
6+
7+
## Onchain Win Recording
8+
9+
When a user wins, their address is recorded onchain via the `/api/win` endpoint using a Coinbase CDP account. See `RrcordOnchainSteps.md` for details. You must set the following in your `.env.local`:
10+
11+
- CDP_API_KEY_ID
12+
- CDP_API_KEY_SECRET
13+
- CDP_WALLET_SECRET
14+
- CDP_SIGNER_ADDRESS
15+
- GAME_CONTRACT_ADDRESS_SEPOLIA
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { NextResponse } from 'next/server';
2+
import { createPublicClient, http, encodeFunctionData } from 'viem';
3+
import { base } from 'viem/chains';
4+
//import { GAME_CONTRACT_ADDRESS } from '@/app/utils/constants';
5+
import { GAME_CONTRACT_ABI } from '@/app/utils/abis/AirdropABI';
6+
import { getCDPAccountByAddress } from '@/lib/cdp/account';
7+
import { cdp } from '@/lib/cdp/client';
8+
9+
const publicClient = createPublicClient({
10+
chain: base,
11+
transport: http(),
12+
});
13+
14+
export async function POST(request: Request) {
15+
try {
16+
const body = await request.json();
17+
const { playerAddress, score, gameId } = body;
18+
19+
console.log('Incoming win request:', { playerAddress, score, gameId });
20+
21+
if (!playerAddress || !score || !gameId) {
22+
return NextResponse.json(
23+
{ error: 'Missing required parameters' },
24+
{ status: 400 }
25+
);
26+
}
27+
28+
const account = await getCDPAccountByAddress(
29+
process.env.CDP_SIGNER_ADDRESS!
30+
);
31+
32+
console.log('Preparing to call contract:', {
33+
contractAddress: '0x9a68a6af680e33c59b7e1c34ecc8bbedf6b5b75b',
34+
functionName: 'recordReward',
35+
cdpSigner: account.address,
36+
});
37+
38+
const txData = encodeFunctionData({
39+
abi: GAME_CONTRACT_ABI,
40+
functionName: 'recordReward',
41+
args: [playerAddress as `0x${string}`, true],
42+
});
43+
44+
const txResult = await cdp.evm.sendTransaction({
45+
address: account.address,
46+
network: 'base',
47+
transaction: {
48+
to: '0x9a68a6af680e33c59b7e1c34ecc8bbedf6b5b75b' as `0x${string}`,
49+
data: txData,
50+
},
51+
});
52+
53+
console.log('Game win transaction hash:', txResult.transactionHash);
54+
55+
await publicClient.waitForTransactionReceipt({
56+
hash: txResult.transactionHash,
57+
});
58+
59+
return NextResponse.json({
60+
success: true,
61+
transactionHash: txResult.transactionHash,
62+
});
63+
} catch (error) {
64+
return NextResponse.json(
65+
{
66+
error: 'Failed to process game win',
67+
details: error instanceof Error ? error.message : 'Unknown error',
68+
},
69+
{ status: 500 }
70+
);
71+
}
72+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use client';
2+
3+
import { useRouter } from 'next/navigation';
4+
import { useEffect, useState } from 'react';
5+
6+
interface Word {
7+
word: string;
8+
description: string;
9+
hints: string[];
10+
}
11+
12+
interface Category {
13+
name: string;
14+
words: Word[];
15+
}
16+
17+
interface Categories {
18+
categories: Category[];
19+
}
20+
21+
export default function CategorySelect() {
22+
const router = useRouter();
23+
const [categories, setCategories] = useState<Category[]>([]);
24+
const [loading, setLoading] = useState(true);
25+
26+
useEffect(() => {
27+
const loadCategories = async () => {
28+
try {
29+
const response = await fetch('/wordCategories.json');
30+
const data: Categories = await response.json();
31+
setCategories(data.categories);
32+
} catch (error) {
33+
console.error('Error loading categories:', error);
34+
} finally {
35+
setLoading(false);
36+
}
37+
};
38+
39+
loadCategories();
40+
}, []);
41+
42+
const handleCategorySelect = (categoryName: string) => {
43+
router.push(`/game/${encodeURIComponent(categoryName)}`);
44+
};
45+
46+
if (loading) {
47+
return (
48+
<div className='flex items-center justify-center min-h-screen'>
49+
<div className='animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500'></div>
50+
</div>
51+
);
52+
}
53+
54+
return (
55+
<div className='min-h-screen bg-white dark:bg-background p-8'>
56+
<div className='max-w-6xl mx-auto'>
57+
{/* Header */}
58+
<div className='text-center mb-12'>
59+
<h1 className='text-4xl font-bold mb-4'>Choose a Category</h1>
60+
<p className='text-gray-600 dark:text-gray-300'>
61+
Select a category to start playing Hangman
62+
</p>
63+
</div>
64+
65+
{/* Categories Grid */}
66+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'>
67+
{categories.map((category) => (
68+
<button
69+
key={category.name}
70+
onClick={() => handleCategorySelect(category.name)}
71+
className='group relative bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-all duration-300'
72+
>
73+
<div className='p-6'>
74+
<h2 className='text-2xl font-bold mb-4 text-center'>
75+
{category.name}
76+
</h2>
77+
<div className='space-y-2'>
78+
<p className='text-gray-600 dark:text-gray-300 text-center'>
79+
{category.words.length} words to guess
80+
</p>
81+
<div className='flex justify-center items-center space-x-2'>
82+
<span className='text-sm text-gray-500 dark:text-gray-400'>
83+
Sample words:
84+
</span>
85+
<span className='text-sm font-medium'>
86+
{category.words
87+
.slice(0, 2)
88+
.map((w) => w.word)
89+
.join(', ')}
90+
</span>
91+
</div>
92+
</div>
93+
</div>
94+
<div className='absolute inset-0 bg-blue-500 opacity-0 group-hover:opacity-10 transition-opacity duration-300'></div>
95+
</button>
96+
))}
97+
</div>
98+
99+
{/* Back Button */}
100+
<div className='text-center mt-8'>
101+
<button
102+
onClick={() => router.push('/')}
103+
className='px-6 py-2 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-white transition-colors duration-200'
104+
>
105+
← Back to Home
106+
</button>
107+
</div>
108+
</div>
109+
</div>
110+
);
111+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use client';
2+
3+
import { useAccount, useSwitchChain, createConfig, http } from 'wagmi';
4+
import { useState } from 'react';
5+
import { AirdropABI } from '../utils/abis/AirdropABI';
6+
import { base } from 'viem/chains';
7+
import { parseEther } from 'viem';
8+
import { encodeFunctionData } from 'viem';
9+
import { useSendCalls } from 'wagmi/experimental';
10+
import { useRouter } from 'next/navigation';
11+
import {
12+
GAME_CONTRACT_ADDRESS,
13+
PAYMASTER_URL,
14+
PLAY_FEE_ETH,
15+
} from '../utils/constants';
16+
17+
interface PlayButtonProps {
18+
onSuccess: () => void;
19+
finalScore?: number;
20+
}
21+
22+
export const config = createConfig({
23+
chains: [base],
24+
transports: {
25+
[base.id]: http(PAYMASTER_URL),
26+
},
27+
});
28+
29+
export function PlayButton({ onSuccess, finalScore }: PlayButtonProps) {
30+
const router = useRouter();
31+
const account = useAccount();
32+
const [isLoading, setIsLoading] = useState(false);
33+
const [error, setError] = useState<string | null>(null);
34+
const [success, setSuccess] = useState(false);
35+
const { switchChain } = useSwitchChain();
36+
const { sendCalls } = useSendCalls({
37+
mutation: {
38+
onSuccess: () => {
39+
setSuccess(true);
40+
onSuccess();
41+
router.push('/category-select');
42+
},
43+
onError: (error) => {
44+
console.error('Error starting game:', error);
45+
setError('Failed to start game. Please try again.');
46+
},
47+
},
48+
});
49+
50+
// const capabilities = useCapabilities({ config });
51+
52+
// console.log(capabilities);
53+
54+
const handlePlay = async () => {
55+
if (!account.isConnected) {
56+
setError('Please connect your wallet first');
57+
return;
58+
}
59+
60+
if (!account.chainId) {
61+
setError('Please connect to Base network');
62+
return;
63+
}
64+
65+
if (account.chainId !== base.id) {
66+
switchChain({ chainId: base.id });
67+
return;
68+
}
69+
70+
setIsLoading(true);
71+
setError(null);
72+
setSuccess(false);
73+
74+
console.log('PAYMASTER_URL', PAYMASTER_URL);
75+
try {
76+
const data = encodeFunctionData({
77+
abi: AirdropABI,
78+
functionName: 'startGame',
79+
args: [true],
80+
});
81+
82+
const calls = [
83+
{
84+
to: GAME_CONTRACT_ADDRESS,
85+
data,
86+
value: parseEther(PLAY_FEE_ETH),
87+
},
88+
];
89+
90+
sendCalls({
91+
calls,
92+
capabilities: {
93+
paymasterService: {
94+
url: 'https://api.developer.coinbase.com/rpc/v1/base/2Ohgm8ABsCDs0AUApVoi5ivZGe2t7eHb',
95+
},
96+
},
97+
});
98+
} catch (err) {
99+
console.error('Error starting game:', err);
100+
setError('Failed to start game. Please try again.');
101+
} finally {
102+
setIsLoading(false);
103+
}
104+
};
105+
106+
return (
107+
<div className='w-full'>
108+
{error && (
109+
<div className='mb-4'>
110+
<p className='text-red-500'>{error}</p>
111+
</div>
112+
)}
113+
{success && (
114+
<div className='mb-4'>
115+
<p className='text-green-500'>Game started successfully! 🎮</p>
116+
</div>
117+
)}
118+
<button
119+
onClick={handlePlay}
120+
disabled={isLoading || !account.isConnected}
121+
className='w-full px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed'
122+
>
123+
{isLoading ? (
124+
<div className='flex items-center justify-center'>
125+
<div className='animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2'></div>
126+
Starting Game...
127+
</div>
128+
) : !account.isConnected ? (
129+
'Connect Wallet'
130+
) : finalScore !== undefined ? (
131+
'Play Again'
132+
) : (
133+
'Start Game'
134+
)}
135+
</button>
136+
</div>
137+
);
138+
}

0 commit comments

Comments
 (0)