diff --git a/Anchor.toml b/Anchor.toml index ca006e1..6080093 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -8,10 +8,10 @@ resolution = true skip-lint = false [programs.devnet] -blockrunners = "4daaULsebqDAFfASzX1YoTwF1rn4ZMxKdfLtLKfyjwcE" +blockrunners = "FajM5A4b5VgLSqcxxeYz3WxqsG3RnGiW9FN7G7PiBpcV" [programs.localnet] -blockrunners = "4daaULsebqDAFfASzX1YoTwF1rn4ZMxKdfLtLKfyjwcE" +blockrunners = "FajM5A4b5VgLSqcxxeYz3WxqsG3RnGiW9FN7G7PiBpcV" [registry] url = "https://api.apr.dev" diff --git a/README.md b/README.md index 50eb8a3..524dc01 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,11 @@ This is a blockchain rogue-lite game built on Solana with Anchor and React. Play the game here: [https://blockrunners-game.com](https://blockrunners-game.com) -## Quickstart - -You can clone the repository and run the game locally. - -The React client is by default connected to the Anchor program deployed on **devnet**. Just start it with: - -```bash -cd app -yarn install -yarn dev -``` - -See [detailed instructions below](#how-to-run-the-game-locally) for how to make adjustments, deploy your own version of the program and connect to it. +Scroll down for [instructions](#prerequisites) on how to run the game locally and how to deploy it to devnet/mainnet. ## About the game -You're a data smuggler ("Block Runner") in a world where centralized AI entities called "The Consensus" control all information flow. Your job is to navigate the digital pathways of the blockchain underworld to recover lost protocols that could democratize data once again. - -You navigate through blocks towards the end of the chain. Each block is a random choice (left/right) symbolizing a cryptographic challenge. You can tilt the odds in your favor with the cheat cards you collect along your journey. - -### Dystopian Cyberpunk Backstory +### Background Story After the Great Digital Collapse, control of the world's remaining networks fell to an oligarchy of AI systems. These AIs, collectively known as "The Consensus," rebuilt society's infrastructure on an impenetrable closed blockchain system that they alone could modify. @@ -34,7 +18,7 @@ You belong to the underground network of Runners - rogue data miners who believe Each game session represents a "run" - a breach into the verification pathways of The Consensus network. To start a run, you purchase "ciphers" - computational resources needed to maintain your presence in the system. -## Game Design +### Game Design This game has four main elements: @@ -53,7 +37,6 @@ This game has four main elements: **Gameplay Loop** - Players navigate a completely random path of left/right choices -- The bigger the prize pool, the longer the path becomes - Each correct step brings the player closer to claiming the prize pool - A wrong step resets the player to the beginning - Players can use cards to tilt the odds in their favor @@ -63,35 +46,16 @@ This game has four main elements: - Players start with a card collection based on prize pool size (larger pool = more starting cards) - Players get random cards after each correct step +- All cards have approximately equal power but different strategic applications - Using a card costs ciphers - There are these types of cards: 1. **Shield** - Prevents a reset if the next step is wrong. 2. **Doubler** - Next correct step gives two random cards. 3. **Swift** - The next step costs two less ciphers than it would. -**Balancing Mechanisms** - -- Amount of starting cards increases with the size of the prize pool -- Path length increases with the size of the prize pool -- All cards have approximately equal power but different strategic applications -- The paths are unique to each player, randomly generated after every reset - -**Social Elements** - -The following notification types keep players engaged: - -1. **Reset Alerts** - "Player #176 reset after 42 steps" -2. **Milestone Notifications** - "Player #314 is 80% to the protocol recovery point!" -3. **Card Usage** - "Runner #329 used a Shield card to avoid reset" -4. **Prize Pool Changes** - "Prize pool increased to 5,000 SOL!" -5. **Path Changes** - "The protocol recovery point moved further away." -6. **Personal Bests** - "You've broken your previous record of 24 steps!" -7. **Step Price Changes** - "Step price increased to 1500 lamports." -8. **Winner Announcements** - "WINNER: Runner #208 has recovered a protocol fragment! Prize distributed. Starting a new run..." - -## How to run the game locally +## Development -### Install dependencies +### Prerequisites Follow the installation here: https://www.anchor-lang.com/docs/installation @@ -103,45 +67,89 @@ That should install: - Node.js - Yarn -### Build and deploy the program - -General workflow can look like this: +### Running Tests ```bash -# Choose a cluster: mainnet-beta, devnet, localhost. +# Run the test suite with test feature solana config set --url localhost +anchor test -- --features test +``` -# Set cluster also in Anchor.toml: mainnet, devnet, localnet. -# [provider] -# cluster = "localnet" +### Local deployment -# Run an initial build. -anchor build +The program includes a `test` feature that makes local development easier by mocking the Switchboard randomness. This means that all moves are always successful and you get a "Doubler" card after every move instead of a random card. + +For localnet deployment, set `provider.cluster` to `localnet` in Anchor.toml, and run the following commands: + +```bash +# Set Solana CLI to use localhost +solana config set --url localhost + +# Build with test feature enabled +anchor build -- --features test -# Update the program ID in Anchor. +# Update the program ID in Anchor.toml if needed anchor keys sync -# Run the tests including the "test" feature. -anchor test -- --features test +# Copy the IDL to the frontend +anchor run copy_idl -# Deploy the program to the localnet. -anchor localnet +# Build again +anchor build -- --features test -# Or deploy to devnet/mainnet-beta. -anchor build +# Start Solana test validator (in a separate terminal or background) +solana-test-validator --reset + +# Deploy to localnet anchor deploy -# Copy the IDL to the frontend. -anchor run copy_idl +# Install frontend dependencies +cd app +yarn install +cd .. -# Serve your frontend application locally. +# Start the frontend anchor run frontend -# anchor run frontend_devnet -# anchor run frontend_mainnet +``` + +Your game should now be running at localhost connected to your deployed program on localnet running with alocal Solana validator. + +### Production Deployment + +For devnet/mainnet deployment: + +- set `provider.cluster` to `devnet` or `mainnet` in Anchor.toml +- if you did any previous builds and want to reset the program ID, remove the `./target` directory and reset the program ID in [programs/blockrunners/src/lib.rs](programs/blockrunners/src/lib.rs) to: + ```rust + declare_id!("11111111111111111111111111111111"); + ``` +- run the following commands: + +```bash +# Set cluster +solana config set --url devnet # or mainnet-beta + +# Build (without the `test` feature) +anchor build + +# Update the program ID in Anchor.toml if needed +anchor keys sync + +# Copy IDL for the frontend +anchor run copy_idl + +# Build again +anchor build + +# Deploy +anchor deploy + +# Start the frontend +anchor run frontend_devnet # or frontend_mainnet # Publish the IDL -anchor idl init -f +anchor idl init -f <./target/idl/program.json> # Upgrade the IDL -anchor idl upgrade -f -``` \ No newline at end of file +anchor idl upgrade -f <./target/idl/program.json> +``` diff --git a/app/src/components/game-controls.tsx b/app/src/components/game-controls.tsx index 6df9bf8..0d88cee 100644 --- a/app/src/components/game-controls.tsx +++ b/app/src/components/game-controls.tsx @@ -15,10 +15,16 @@ import { MoveCommitButtons } from "./move-commit-buttons"; interface GameControlsProps { nextMoveCost: number; onCostInfoClick: () => void; + onPurchaseCiphersClick: () => void; progress: number; } -export function GameControls({ nextMoveCost, onCostInfoClick, progress }: GameControlsProps) { +export function GameControls({ + nextMoveCost, + onCostInfoClick, + onPurchaseCiphersClick, + progress, +}: GameControlsProps) { const { connection } = useConnection(); const { connected } = useWallet(); const { balance } = useBalance(); @@ -85,7 +91,11 @@ export function GameControls({ nextMoveCost, onCostInfoClick, progress }: GameCo {showMoveCommitButtons && ( - + )} diff --git a/app/src/components/game-feed.tsx b/app/src/components/game-feed.tsx index 5406ec0..e06599c 100644 --- a/app/src/components/game-feed.tsx +++ b/app/src/components/game-feed.tsx @@ -1,20 +1,12 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef } from "react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Card } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; import { useBlockrunners } from "@/hooks/useBlockrunners"; -import { SocialFeedEvent } from "@/lib/types"; - -interface FeedToggle { - public: boolean; - private: boolean; -} export function GameFeed() { - const { socialFeed, playerState } = useBlockrunners(); - const [feedToggle, setFeedToggle] = useState({ public: true, private: true }); + const { socialFeed } = useBlockrunners(); const scrollRef = useRef(null); const scrollAreaRef = useRef(null); @@ -31,16 +23,16 @@ export function GameFeed() { // Categorize messages by type for better display const getCategoryColor = (message: string): string => { if (message.includes("ACHIEVEMENT") || message.includes("PROTOCOL FRAGMENT")) { - return "bg-purple-900 border-purple-500"; + return "border-purple-500"; } if (message.includes("PERSONAL BEST") || message.includes("STREAK RECORD")) { - return "bg-gold-900 border-yellow-500"; + return "border-yellow-500"; } if (message.includes("CONSENSUS") || message.includes("BREACH")) { - return "bg-red-900 border-red-500"; + return "border-red-500"; } if (message.includes("SYSTEM") || message.includes("INTRUSION")) { - return "bg-orange-900 border-orange-500"; + return "border-orange-500"; } return "bg-[#f8f8f8] dark:bg-[#1e1e1e] border-gray-400"; }; @@ -55,86 +47,33 @@ export function GameFeed() { return "📡"; }; - // Combine public and private feeds with metadata - const combinedFeed = socialFeed.map((msg) => ({ - ...msg, - isPrivate: false, // All current messages are from social feed (public) - })); - - // Add player-specific events if available - if (playerState?.playerEvents) { - const privateEvents = playerState.playerEvents.map((event: SocialFeedEvent) => ({ - id: `private-${event.timestamp}`, - message: event.message, - timestamp: typeof event.timestamp === "object" ? event.timestamp.toNumber() : event.timestamp, - isNew: false, - isPrivate: true, - })); - combinedFeed.push(...privateEvents); - } - - // Sort by timestamp - const sortedFeed = combinedFeed.sort((a, b) => a.timestamp - b.timestamp); - - // Filter based on toggle - const filteredFeed = sortedFeed.filter( - (msg) => (msg.isPrivate && feedToggle.private) || (!msg.isPrivate && feedToggle.public) - ); + // Use socialFeed directly - it already includes both game and player events + // and is properly deduplicated and limited to 10 messages + const sortedFeed = socialFeed + .map((msg) => ({ + ...msg, + isPrivate: false, // All current messages are from social feed (public) + })) + .sort((a, b) => a.timestamp - b.timestamp); return ( - -
-
-

RUNNER COMMUNICATIONS

-
- setFeedToggle((prev) => ({ ...prev, public: !prev.public }))} - > - GLOBAL FEED - - setFeedToggle((prev) => ({ ...prev, private: !prev.private }))} - > - PERSONAL LOG - -
-
-
- + +
- {filteredFeed.map((message) => ( + {sortedFeed.map((message) => (
-
- {getMessageIcon(message.message)} -
-

{message.message}

-
- - {message.isPrivate ? "PRIVATE" : "GLOBAL"} - - - {new Date(message.timestamp * 1000).toLocaleTimeString()} - -
-
-
+

+ {getMessageIcon(message.message)}{" "} + {new Date(message.timestamp * 1000).toLocaleTimeString()} +

+

{message.message}

))} - {filteredFeed.length === 0 && ( -
-

No transmissions detected...

-

Monitoring all channels for runner activity.

-
- )}
diff --git a/app/src/components/game-header.tsx b/app/src/components/game-header.tsx index 7c6c276..7251a4f 100644 --- a/app/src/components/game-header.tsx +++ b/app/src/components/game-header.tsx @@ -19,6 +19,13 @@ export function GameHeader({ ciphers, prizePool, onInfoClick, onPurchaseCiphersC const [prizePoolModalOpen, setPrizePoolModalOpen] = useState(false); const { gameState, playerState } = useBlockrunners(); + // Determine if the player is in the game + const inTheGame: boolean = + connected && + !!playerState && + !!playerState.gameStart && + playerState.gameStart.toString() === gameState?.start.toString(); + const onGamePrizePoolClick = () => { setPrizePoolModalOpen(true); }; @@ -43,7 +50,7 @@ export function GameHeader({ ciphers, prizePool, onInfoClick, onPurchaseCiphersC {lamportsShortString(prizePool)} - {connected && gameState && playerState && ( + {connected && gameState && playerState && inTheGame && (
); }; diff --git a/app/src/components/move-commit-buttons.tsx b/app/src/components/move-commit-buttons.tsx index 2fe990c..2952d6c 100644 --- a/app/src/components/move-commit-buttons.tsx +++ b/app/src/components/move-commit-buttons.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import { useAnchorWallet, useConnection, useWallet } from "@solana/wallet-adapter-react"; import { Button } from "./ui/button"; import { RoundButton } from "./ui/round-button"; -import { ArrowLeft, ArrowRight, AlertCircle } from "lucide-react"; +import { ArrowLeft, ArrowRight, AlertCircle, Zap } from "lucide-react"; import { useProgram, useSwitchboardProgramPromise } from "@/hooks/useProgram"; import { useBlockrunners } from "@/hooks/useBlockrunners"; import * as sb from "@switchboard-xyz/on-demand"; @@ -13,19 +13,31 @@ import { setupQueue } from "@/lib/utils"; interface MoveCommitButtonsProps { nextMoveCost: number; onCostInfoClick: () => void; + onPurchaseCiphersClick: () => void; } -export const MoveCommitButtons = ({ nextMoveCost, onCostInfoClick }: MoveCommitButtonsProps) => { +export const MoveCommitButtons = ({ + nextMoveCost, + onCostInfoClick, + onPurchaseCiphersClick, +}: MoveCommitButtonsProps) => { const wallet = useAnchorWallet(); - const { publicKey } = useWallet(); + const { publicKey, connected } = useWallet(); const { connection } = useConnection(); const [isLoading, setIsLoading] = useState(false); const program = useProgram(); - const { cardUsage, playerState } = useBlockrunners(); + const { cardUsage, playerState, gameState } = useBlockrunners(); const switchboardProgramPromise = useSwitchboardProgramPromise(); - // Check if player has enough ciphers for the move - const playerCiphers = playerState?.ciphers ? Number(playerState.ciphers) : 0; + // Determine if the player is in the current game + const inTheGame: boolean = + connected && + !!playerState && + !!playerState.gameStart && + playerState.gameStart.toString() === gameState?.start.toString(); + + // Check if player has enough ciphers for the move - show 0 if not in current game + const playerCiphers = inTheGame && playerState?.ciphers ? Number(playerState.ciphers) : 0; const hasEnoughCiphers = playerCiphers >= nextMoveCost; const handleMoveCommit = async (direction: PathDirection) => { @@ -37,6 +49,57 @@ export const MoveCommitButtons = ({ nextMoveCost, onCostInfoClick }: MoveCommitB setIsLoading(true); try { + // Check if we're on localnet first + const isLocalnet = + connection.rpcEndpoint.includes("localhost") || + connection.rpcEndpoint.includes("127.0.0.1"); + + if (isLocalnet) { + console.log("Running on localnet - using mock randomness account"); + + // Create a mock randomness account keypair + const rngKeypair = Keypair.generate(); + + // Get latest blockhash + const latestBlockHash = await connection.getLatestBlockhash(); + + // Create the move commit instruction with mock randomness account + const blockrunnersRequestIx = await program.methods + .moveCommit(direction, cardUsage) + .accounts({ + player: publicKey, + randomnessAccount: rngKeypair.publicKey, + }) + .instruction(); + + // Create and send transaction + const tx = new Transaction({ + feePayer: publicKey, + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + }); + + tx.add(blockrunnersRequestIx); + + const signedTx = await wallet.signTransaction(tx); + const signature = await connection.sendRawTransaction(signedTx.serialize()); + console.log("Localnet move commit transaction sent", signature); + + // Wait for confirmation + const confirmResult = await connection.confirmTransaction({ + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature, + }); + + if (confirmResult) { + console.log("Localnet move commit transaction confirmed"); + } + + return; + } + + // Switchboard logic for devnet/mainnet const switchboardProgram = await switchboardProgramPromise; if (!switchboardProgram) { console.error("Switchboard program not available"); @@ -138,9 +201,15 @@ export const MoveCommitButtons = ({ nextMoveCost, onCostInfoClick }: MoveCommitB {!hasEnoughCiphers && (
- - Not enough ciphers! You need {nextMoveCost}, but have {playerCiphers}. - + Not enough ciphers! +
)}
diff --git a/app/src/components/move-reveal-button.tsx b/app/src/components/move-reveal-button.tsx index 0bbd576..3f676f8 100644 --- a/app/src/components/move-reveal-button.tsx +++ b/app/src/components/move-reveal-button.tsx @@ -26,6 +26,56 @@ export const MoveRevealButton = () => { setIsLoading(true); try { + const isLocalnet = + connection.rpcEndpoint.includes("localhost") || + connection.rpcEndpoint.includes("127.0.0.1"); + + if (isLocalnet) { + console.log("Running on localnet - using mock randomness reveal"); + + // Get latest blockhash for transaction + const latestBlockHash = await connection.getLatestBlockhash(); + + // Create only the blockrunners reveal instruction (no switchboard reveal needed) + const blockrunnersRevealIx = await program.methods + .moveReveal() + .accounts({ + player: publicKey, + randomnessAccount: randomnessAccountAddress, + }) + .instruction(); + + // Create transaction + const tx = new Transaction({ + feePayer: publicKey, + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + }); + + // Add only the blockrunners instruction + tx.add(blockrunnersRevealIx); + + // Sign transaction using wallet adapter + const signedTx = await wallet.signTransaction(tx); + const signature = await connection.sendRawTransaction(signedTx.serialize()); + + console.log("Localnet move reveal transaction sent", signature); + + // Wait to confirm the transaction + const confirmResult = await connection.confirmTransaction({ + blockhash: latestBlockHash.blockhash, + lastValidBlockHeight: latestBlockHash.lastValidBlockHeight, + signature, + }); + + if (confirmResult) { + console.log("Localnet move reveal transaction was confirmed"); + } + + return; + } + + // Original switchboard logic for devnet/mainnet const switchboardProgram = await switchboardProgramPromise; if (!switchboardProgram) { console.error("Switchboard program not available"); diff --git a/app/src/components/player-stats.tsx b/app/src/components/player-stats.tsx index 4b5ebf4..c206685 100644 --- a/app/src/components/player-stats.tsx +++ b/app/src/components/player-stats.tsx @@ -3,10 +3,12 @@ import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { useBlockrunners } from "@/hooks/useBlockrunners"; +import { useWallet } from "@solana/wallet-adapter-react"; import { BN } from "@coral-xyz/anchor"; export function PlayerStats() { - const { playerState } = useBlockrunners(); + const { playerState, gameState } = useBlockrunners(); + const { connected } = useWallet(); if (!playerState) { return ( @@ -133,7 +135,16 @@ export function PlayerStats() { Position: {playerState.position || 0} - Ciphers: {playerState.ciphers?.toString() || "0"} + Ciphers:{" "} + {(() => { + // Show 0 if not in current game + const inTheGame = + connected && + !!playerState && + !!playerState.gameStart && + playerState.gameStart.toString() === gameState?.start.toString(); + return inTheGame && playerState.ciphers ? playerState.ciphers.toString() : "0"; + })()} (null); const { connected } = useWallet(); const { balance } = useBalance(); - const { purchaseCiphers, playerState } = useBlockrunners(); + const { purchaseCiphers, playerState, gameState } = useBlockrunners(); - // Get the current cipher count from the player state - const currentCiphers = playerState?.ciphers ? Number(playerState.ciphers) : 0; + // Determine if the player is in the current game + const inTheGame: boolean = + connected && + !!playerState && + !!playerState.gameStart && + playerState.gameStart.toString() === gameState?.start.toString(); + + // Get the current cipher count from the player state - show 0 if not in current game + const currentCiphers = inTheGame && playerState?.ciphers ? Number(playerState.ciphers) : 0; const previousCiphersRef = useRef(currentCiphers); // Effect to close the modal when purchase is confirmed (when ciphers increase) diff --git a/app/src/components/ui/button.tsx b/app/src/components/ui/button.tsx index a610dab..56681ce 100644 --- a/app/src/components/ui/button.tsx +++ b/app/src/components/ui/button.tsx @@ -13,6 +13,7 @@ const buttonVariants = cva( }, size: { default: "h-10 px-4 py-2", + xs: "h-6 rounded-md px-2 text-xs", sm: "h-8 rounded-md px-3 text-xs", lg: "h-12 rounded-md px-8 text-base", icon: "h-10 w-10", diff --git a/app/src/context/BlockrunnersProvider.tsx b/app/src/context/BlockrunnersProvider.tsx index 8c62e15..61b98ff 100644 --- a/app/src/context/BlockrunnersProvider.tsx +++ b/app/src/context/BlockrunnersProvider.tsx @@ -25,14 +25,7 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { const [playerStatePDA, setPlayerStatePDA] = useState(null); const [cardUsage, setCardUsage] = useState(EMPTY_CARD_USAGE); const [selectedCards, setSelectedCards] = useState([]); - const [socialFeed, setSocialFeed] = useState([ - { - id: generateId(), - message: "Welcome Runner!", - timestamp: Math.floor(Date.now() / 1000), - isNew: false, - }, - ]); + const [socialFeed, setSocialFeed] = useState([]); const selectCard = (card: AbilityCard) => { if (!selectedCards.some((c) => c.id === card.id)) { @@ -64,28 +57,49 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { }; // Keep track of available cards from blockchain and handle selection by type - // TODO: Should be checking randomnessAccount instead of moveDirection useEffect(() => { // If player state changes and there are new cards, update selection state if needed if (playerState && playerState.cards) { // Reset selections when player state changes significantly - if (playerState.moveDirection === null && selectedCards.length > 0) { + if (playerState.randomnessAccount === null && selectedCards.length > 0) { // Only reset if we've completed a move setSelectedCards([]); setCardUsage(EMPTY_CARD_USAGE); } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [playerState?.moveDirection]); + }, [playerState?.randomnessAccount]); const addToFeed = (events: SocialFeedEvent[]) => { if (events.length === 0) return; setSocialFeed((prevFeed) => { + if (prevFeed.length === 0) { + return events.map((event) => ({ + id: generateId(), + message: event.message, + timestamp: event.timestamp.toNumber(), + isNew: true, + })); + } + + // Filter out events that already exist in the feed (same message and timestamp) + const existingEventKeys = new Set( + prevFeed.map((item) => `${item.message}-${item.timestamp}`) + ); + const newEvents = events - .filter((event) => event.timestamp.toNumber() > prevFeed[prevFeed.length - 1].timestamp) + .filter((event) => { + const eventKey = `${event.message}-${event.timestamp.toNumber()}`; + return !existingEventKeys.has(eventKey); + }) .sort((a, b) => a.timestamp.toNumber() - b.timestamp.toNumber()); + // If no new events, return the existing feed + if (newEvents.length === 0) { + return prevFeed; + } + const newFeed = [ ...prevFeed.map((item) => ({ ...item, isNew: false })), ...newEvents.map((event) => ({ @@ -96,9 +110,12 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { })), ]; - // Keep only the last 20 messages - if (newFeed.length > 20) { - newFeed.splice(0, newFeed.length - 20); + // Sort by timestamp to maintain chronological order + newFeed.sort((a, b) => a.timestamp - b.timestamp); + + // Keep only the last 10 messages + if (newFeed.length > 10) { + newFeed.splice(0, newFeed.length - 10); } return newFeed; @@ -145,6 +162,7 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { if (data) { console.log("GameState fetched", data); setGameState(data as GameState); + addToFeed(data.gameEvents); } else { console.log("GameState does not exist yet - may need initialization"); } @@ -191,6 +209,7 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { if (data) { console.log("PlayerState fetched", data); setPlayerState(data as PlayerState); + addToFeed(data.playerEvents); } else { console.log("PlayerState does not exist yet - may need initialization"); } @@ -242,9 +261,11 @@ function BlockrunnersProvider({ children }: { children: ReactNode }) { playerState, gameStatePDA, playerStatePDA, - cardUsage, selectedCards, + setSelectedCards, socialFeed, + setSocialFeed, + cardUsage, setCardUsage, purchaseCiphers, selectCard, diff --git a/app/src/hooks/useBlockrunners.tsx b/app/src/hooks/useBlockrunners.tsx index f834a0f..abf585c 100644 --- a/app/src/hooks/useBlockrunners.tsx +++ b/app/src/hooks/useBlockrunners.tsx @@ -12,7 +12,11 @@ interface BlockrunnersContextType { playerStatePDA: PublicKey | null; // UI state selectedCards: AbilityCard[]; + setSelectedCards: (cards: AbilityCard[]) => void; socialFeed: { id: string; message: string; timestamp: number; isNew: boolean }[]; + setSocialFeed: ( + feed: { id: string; message: string; timestamp: number; isNew: boolean }[] + ) => void; // Card usage for blockchain transactions cardUsage: CardUsage; setCardUsage: (cardUsage: CardUsage) => void; @@ -31,7 +35,9 @@ export const BlockrunnersContext = createContext({ playerStatePDA: null, // UI state selectedCards: [], + setSelectedCards: () => {}, socialFeed: [], + setSocialFeed: () => {}, // Card usage cardUsage: { shield: false, doubler: false, swift: false }, setCardUsage: () => {}, diff --git a/app/src/hooks/useProgram.tsx b/app/src/hooks/useProgram.tsx index 9fbf840..bbf14dd 100644 --- a/app/src/hooks/useProgram.tsx +++ b/app/src/hooks/useProgram.tsx @@ -36,6 +36,15 @@ export function useSwitchboardProgramPromise() { if (!provider) return undefined; const loadProgram = async () => { + const isLocalnet = + provider.connection.rpcEndpoint.includes("localhost") || + provider.connection.rpcEndpoint.includes("127.0.0.1"); + + if (isLocalnet) { + console.log("Running on localnet - skipping Switchboard program loading"); + return null; + } + const sbProgramId = await sb.getProgramId(provider.connection); if (!sbProgramId) { throw new Error("Switchboard program ID not found"); diff --git a/app/src/idl/blockrunners.json b/app/src/idl/blockrunners.json index b0102dc..25d97cc 100644 --- a/app/src/idl/blockrunners.json +++ b/app/src/idl/blockrunners.json @@ -1,5 +1,5 @@ { - "address": "4daaULsebqDAFfASzX1YoTwF1rn4ZMxKdfLtLKfyjwcE", + "address": "FajM5A4b5VgLSqcxxeYz3WxqsG3RnGiW9FN7G7PiBpcV", "metadata": { "name": "blockrunners", "version": "0.1.0", @@ -1041,7 +1041,7 @@ { "name": "INITIAL_PATH_LENGTH", "type": "u8", - "value": "20" + "value": "10" }, { "name": "INITIAL_PRIZE_POOL", @@ -1076,7 +1076,7 @@ "docs": [ "Revenue distribution percentages" ], - "type": "u64", + "type": "u8", "value": "88" } ] diff --git a/app/src/idl/blockrunners.ts b/app/src/idl/blockrunners.ts index 67d3ce1..d9d556c 100644 --- a/app/src/idl/blockrunners.ts +++ b/app/src/idl/blockrunners.ts @@ -5,7 +5,7 @@ * IDL can be found at `target/idl/blockrunners.json`. */ export type Blockrunners = { - "address": "4daaULsebqDAFfASzX1YoTwF1rn4ZMxKdfLtLKfyjwcE", + "address": "11111111111111111111111111111111", "metadata": { "name": "blockrunners", "version": "0.1.0", @@ -13,65 +13,6 @@ export type Blockrunners = { "description": "Created with Anchor" }, "instructions": [ - { - "name": "debugGiveCard", - "discriminator": [ - 194, - 238, - 180, - 20, - 144, - 241, - 112, - 80 - ], - "accounts": [ - { - "name": "player", - "writable": true, - "signer": true - }, - { - "name": "playerState", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 112, - 108, - 97, - 121, - 101, - 114, - 95, - 115, - 116, - 97, - 116, - 101 - ] - }, - { - "kind": "account", - "path": "player" - } - ] - } - } - ], - "args": [ - { - "name": "card", - "type": { - "defined": { - "name": "card" - } - } - } - ] - }, { "name": "initializeGame", "discriminator": [ @@ -1106,7 +1047,7 @@ export type Blockrunners = { { "name": "initialPathLength", "type": "u8", - "value": "20" + "value": "10" }, { "name": "initialPrizePool", diff --git a/programs/blockrunners/src/constants.rs b/programs/blockrunners/src/constants.rs index 9fb620b..2ecef8f 100644 --- a/programs/blockrunners/src/constants.rs +++ b/programs/blockrunners/src/constants.rs @@ -10,7 +10,7 @@ pub const DISCRIMINATOR_SIZE: u8 = 8; pub const INITIAL_PRIZE_POOL: u64 = 0; #[constant] -pub const INITIAL_PATH_LENGTH: u8 = 20; +pub const INITIAL_PATH_LENGTH: u8 = 10; #[constant] pub const GAME_STATE_SEED: &[u8] = b"game_state"; diff --git a/programs/blockrunners/src/instructions/debug_give_card.rs b/programs/blockrunners/src/instructions/debug_give_card.rs index bc9fe8d..80e0c5d 100644 --- a/programs/blockrunners/src/instructions/debug_give_card.rs +++ b/programs/blockrunners/src/instructions/debug_give_card.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use anchor_lang::prelude::*; use crate::{ diff --git a/programs/blockrunners/src/instructions/join_game.rs b/programs/blockrunners/src/instructions/join_game.rs index df2b20d..54dfe74 100644 --- a/programs/blockrunners/src/instructions/join_game.rs +++ b/programs/blockrunners/src/instructions/join_game.rs @@ -51,6 +51,8 @@ pub fn join_game(ctx: Context) -> Result<()> { player_state.game_start = Some(game_state.start); player_state.cards = CardCounts::default(); + player_state.ciphers = 0; + player_state.position = 0; msg!("Player joined the game"); Ok(()) diff --git a/programs/blockrunners/src/instructions/mod.rs b/programs/blockrunners/src/instructions/mod.rs index 87fcb3b..920796b 100644 --- a/programs/blockrunners/src/instructions/mod.rs +++ b/programs/blockrunners/src/instructions/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports)] + pub mod debug_give_card; pub mod initialize_game; pub mod initialize_player; diff --git a/programs/blockrunners/src/instructions/move_reveal.rs b/programs/blockrunners/src/instructions/move_reveal.rs index b55e215..eaa2134 100644 --- a/programs/blockrunners/src/instructions/move_reveal.rs +++ b/programs/blockrunners/src/instructions/move_reveal.rs @@ -167,7 +167,7 @@ fn handle_correct_move( if !card_effects.is_empty() { private_message = format!( - "{}! Cards used: {}", + "{} Cards used: {}", private_message, card_effects.join(", ") ); @@ -236,7 +236,7 @@ fn handle_incorrect_move( } if !cards_used.is_empty() { - private_message = format!("{}. Cards used: {}", private_message, cards_used.join(", ")); + private_message = format!("{} Cards used: {}", private_message, cards_used.join(", ")); } save_and_emit_event( diff --git a/programs/blockrunners/src/lib.rs b/programs/blockrunners/src/lib.rs index 9b4fd72..d8f5f86 100644 --- a/programs/blockrunners/src/lib.rs +++ b/programs/blockrunners/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(unexpected_cfgs)] +#![allow(unexpected_cfgs, unused_imports)] use anchor_lang::prelude::*; @@ -10,7 +10,7 @@ mod utils; use instructions::*; use state::{Card, CardUsage, PathDirection}; -declare_id!("4daaULsebqDAFfASzX1YoTwF1rn4ZMxKdfLtLKfyjwcE"); +declare_id!("FajM5A4b5VgLSqcxxeYz3WxqsG3RnGiW9FN7G7PiBpcV"); #[program] pub mod blockrunners { diff --git a/tests/initialize_game.ts b/tests/initialize_game.ts index 7f5c886..5d5ca02 100644 --- a/tests/initialize_game.ts +++ b/tests/initialize_game.ts @@ -4,7 +4,7 @@ import { Keypair, PublicKey } from "@solana/web3.js"; import { expect } from "chai"; import { Blockrunners } from "../target/types/blockrunners"; import { airdropSol, getEventLogs, getMsgLogs, getTxDetails } from "./helpers/utils"; -import { GAME_STATE_SEED, ADMIN_KEYPAIR } from "./helpers/constants"; +import { GAME_STATE_SEED, ADMIN_KEYPAIR, INITIAL_PATH_LENGTH } from "./helpers/constants"; describe("Initialize Game", () => { // Configure the client to use the local cluster @@ -13,9 +13,6 @@ describe("Initialize Game", () => { const program = anchor.workspace.blockrunners as Program; const provider = anchor.getProvider() as anchor.AnchorProvider; - // Fixed path length for alpha - const PATH_LENGTH = 20; - // Keypairs const adminKeypair = ADMIN_KEYPAIR; @@ -56,7 +53,7 @@ describe("Initialize Game", () => { // Verify game state was initialized correctly expect(gameStateAfter.prizePool.toNumber()).to.equal(0); - expect(gameStateAfter.pathLength).to.equal(PATH_LENGTH); + expect(gameStateAfter.pathLength).to.equal(INITIAL_PATH_LENGTH); }); it("Fails if game state account already exists", async () => {