Skip to content
Merged
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
4 changes: 2 additions & 2 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
140 changes: 74 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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:

Expand All @@ -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
Expand All @@ -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

Expand All @@ -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 <programId> -f <target/idl/program.json>
anchor idl init <programId> -f <./target/idl/program.json>

# Upgrade the IDL
anchor idl upgrade <programId> -f <target/idl/program.json>
```
anchor idl upgrade <programId> -f <./target/idl/program.json>
```
14 changes: 12 additions & 2 deletions app/src/components/game-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -85,7 +91,11 @@ export function GameControls({ nextMoveCost, onCostInfoClick, progress }: GameCo
</div>

{showMoveCommitButtons && (
<MoveCommitButtons nextMoveCost={nextMoveCost} onCostInfoClick={onCostInfoClick} />
<MoveCommitButtons
nextMoveCost={nextMoveCost}
onCostInfoClick={onCostInfoClick}
onPurchaseCiphersClick={onPurchaseCiphersClick}
/>
)}
</div>

Expand Down
105 changes: 22 additions & 83 deletions app/src/components/game-feed.tsx
Original file line number Diff line number Diff line change
@@ -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<FeedToggle>({ public: true, private: true });
const { socialFeed } = useBlockrunners();

const scrollRef = useRef<HTMLDivElement>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
Expand All @@ -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";
Comment on lines +26 to 37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix inconsistent return values in getCategoryColor function.

The function returns border classes for specific categories but background/border classes for the default case, which is inconsistent.

-    return "bg-[#f8f8f8] dark:bg-[#1e1e1e] border-gray-400";
+    return "border-gray-400";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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";
return "border-purple-500";
}
if (message.includes("PERSONAL BEST") || message.includes("STREAK RECORD")) {
return "border-yellow-500";
}
if (message.includes("CONSENSUS") || message.includes("BREACH")) {
return "border-red-500";
}
if (message.includes("SYSTEM") || message.includes("INTRUSION")) {
return "border-orange-500";
}
return "border-gray-400";
🤖 Prompt for AI Agents
In app/src/components/game-feed.tsx between lines 26 and 37, the
getCategoryColor function returns only border color classes for specific
categories but returns both background and border classes in the default case,
causing inconsistency. Modify the default return statement to only return a
border color class consistent with the other cases, removing the background
color classes.

};
Expand All @@ -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 (
<Card className="flex-1 overflow-hidden border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] min-h-[30vh]">
<div className="p-2 border-b-2 border-black bg-gray-100 dark:bg-gray-900">
<div className="flex items-center justify-between mb-2">
<h3 className="font-mono font-bold text-sm">RUNNER COMMUNICATIONS</h3>
<div className="flex gap-2">
<Badge
variant={feedToggle.public ? "default" : "outline"}
className="cursor-pointer font-mono text-xs"
onClick={() => setFeedToggle((prev) => ({ ...prev, public: !prev.public }))}
>
GLOBAL FEED
</Badge>
<Badge
variant={feedToggle.private ? "default" : "outline"}
className="cursor-pointer font-mono text-xs"
onClick={() => setFeedToggle((prev) => ({ ...prev, private: !prev.private }))}
>
PERSONAL LOG
</Badge>
</div>
</div>
</div>
<ScrollArea className="h-full p-4" ref={scrollAreaRef}>
<Card className="h-full flex flex-col overflow-hidden border-4 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}>
<div className="space-y-2" ref={scrollRef}>
{filteredFeed.map((message) => (
{sortedFeed.map((message) => (
<div
key={message.id}
className={`p-3 border-2 rounded-md transition-all ${getCategoryColor(
message.message
)} ${message.isNew ? "animate-pulse" : ""}`}
>
<div className="flex items-start gap-2">
<span className="text-sm">{getMessageIcon(message.message)}</span>
<div className="flex-1">
<p className="font-mono text-xs leading-relaxed text-white">{message.message}</p>
<div className="flex items-center justify-between mt-1">
<Badge variant="outline" className="text-xs">
{message.isPrivate ? "PRIVATE" : "GLOBAL"}
</Badge>
<span className="text-xs text-gray-400 font-mono">
{new Date(message.timestamp * 1000).toLocaleTimeString()}
</span>
</div>
</div>
</div>
<p className="text-xs text-gray-400 font-mono">
{getMessageIcon(message.message)}{" "}
{new Date(message.timestamp * 1000).toLocaleTimeString()}
</p>
<p className="font-mono text-xs leading-relaxed">{message.message}</p>
</div>
))}
{filteredFeed.length === 0 && (
<div className="text-center py-8 text-gray-500 font-mono">
<p>No transmissions detected...</p>
<p className="text-xs mt-2">Monitoring all channels for runner activity.</p>
</div>
)}
</div>
</ScrollArea>
</Card>
Expand Down
Loading
Loading