Skip to content

Commit 89c5db3

Browse files
committed
feat: add create-solana-game script with preset-lumberjack
1 parent c51b5ae commit 89c5db3

File tree

255 files changed

+83679
-1041
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

255 files changed

+83679
-1041
lines changed

add-template-suffix.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as fs from 'fs';
2+
const path = process.argv[2];
3+
4+
if (!path) {
5+
console.error('Path is required.');
6+
process.exit(1);
7+
}
8+
9+
const suffix = '.template';
10+
11+
console.log(`Renaming files in ${path}...`);
12+
const files = getRecursiveFileList(path).filter(
13+
(file) => !file.endsWith(suffix)
14+
);
15+
16+
if (!files.length) {
17+
console.log('No files to rename.');
18+
process.exit(0);
19+
}
20+
21+
for (const file of files) {
22+
const newFile = `${file}${suffix}`;
23+
fs.renameSync(file, newFile);
24+
console.log(` - Renamed ${file} to ${newFile}`);
25+
}
26+
27+
// Point method at path and return a list of all the files in the directory recursively
28+
function getRecursiveFileList(path: string): string[] {
29+
const files: string[] = [];
30+
31+
const items = fs.readdirSync(path);
32+
items.forEach((item) => {
33+
// Check out if it's a directory or a file
34+
const isDir = fs.statSync(`${path}/${item}`).isDirectory();
35+
if (isDir) {
36+
// If it's a directory, recursively call the method
37+
files.push(...getRecursiveFileList(`${path}/${item}`));
38+
} else {
39+
// If it's a file, add it to the array of files
40+
files.push(`${path}/${item}`);
41+
}
42+
});
43+
44+
return files;
45+
}

packages/create-solana-game/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"name": "create-solana-game",
33
"version": "0.0.1",
44
"dependencies": {
5-
"create-nx-workspace": "16.10.0"
5+
"create-nx-workspace": "16.10.0",
6+
"tslib": "^2.3.0"
67
},
78
"type": "commonjs",
89
"main": "./src/index.js",

packages/preset-lumberjack/src/generators/preset/__snapshots__/generator.spec.ts.snap

Lines changed: 1486 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
This game uses gum session keys for auto approval of transactions. Its ment as a starter game for on chain game.
2+
There is a js and a unity client for this game.
3+
Note that neither the program nor session keys are audited. Use at your own risk.
4+
5+
How to run this example:
6+
Follow the installation here: https://www.anchor-lang.com/docs/installation
7+
make sure to install solana CLI version 1.14.20 not the 1.16.x version
8+
sh -c "$(curl -sSfL https://release.solana.com/v1.14.20/install)"
9+
10+
Anchor program
11+
1. Install the [Anchor CLI](https://project-serum.github.io/anchor/getting-started/installation.html)
12+
2. `cd lumberjack` `cd program` to end the program directory
13+
3. Run `anchor build` to build the program
14+
4. Run `anchor deploy` to deploy the program
15+
5. Copy the program id from the terminal into the lib.rs, anchor.toml and within the unity project in the LumberjackService and if you use js in the anchor.ts file
16+
6. Build and deploy again
17+
18+
Next js client
19+
1. Install [Node.js](https://nodejs.org/en/download/)
20+
2. Copy the program id into app/utils/anchor.ts
21+
2. `cd lumberjack` `cd app` to end the app directory
22+
3. Run `yarn install` to install node modules
23+
4. Run `yarn dev` to start the client
24+
5. After doing changes to the anchor program make sure to copy over the types from the program into the client so you can use them
25+
26+
Unity client
27+
1. Install [Unity](https://unity.com/)
28+
2. Open the lumberjack scene
29+
3. Hit play
30+
4. After doing changes to the anchor program make sure to regenerate the C# client: https://solanacookbook.com/gaming/porting-anchor-to-unity.html#generating-the-client
31+
Its done like this (after you have build the program):
32+
cd program
33+
dotnet tool install Solana.Unity.Anchor.Tool <- run once
34+
dotnet anchorgen -i target/idl/lumberjack.json -o target/idl/Lumberjack.cs
35+
36+
then copy the c# code into the unity project
37+
38+
To connect to local host from Unity add these links on the wallet holder game object:
39+
http://localhost:8899
40+
ws://localhost:8900
41+
42+
Here are two videos explaining the energy logic and session keys:
43+
Session keys:
44+
https://www.youtube.com/watch?v=oKvWZoybv7Y&t=17s&ab_channel=Solana
45+
Energy system:
46+
https://www.youtube.com/watch?v=YYQtRCXJBgs&t=4s&ab_channel=Solana
47+
48+
# Energy System
49+
50+
Many casual games in traditional gaming use energy systems. This is how you can build it on chain.
51+
Recommended to start with the Solana cookbook [Hello world example]([https://unity.com/](https://solanacookbook.com/gaming/hello-world.html#getting-started-with-your-first-solana-game)).
52+
53+
## Anchor program
54+
55+
Here we will build a program which refills energy over time which the player can then use to perform actions in the game.
56+
In our example it will be a lumber jack which chops trees. Every tree will reward on wood and cost one energy.
57+
58+
### Creating the player account
59+
60+
First the player needs to create an account which saves the state of our player. Notice the last_login time which will save the current unix time stamp of the player he interacts with the program.
61+
Like this we will be able to calculate how much energy the player has at a certain point in time.
62+
We also have a value for wood which will store the wood the lumber jack chucks in the game.
63+
64+
```rust
65+
66+
pub fn init_player(ctx: Context<InitPlayer>) -> Result<()> {
67+
ctx.accounts.player.energy = MAX_ENERGY;
68+
ctx.accounts.player.last_login = Clock::get()?.unix_timestamp;
69+
Ok(())
70+
}
71+
72+
...
73+
74+
#[derive(Accounts)]
75+
pub struct InitPlayer <'info> {
76+
#[account(
77+
init,
78+
payer = signer,
79+
space = 1000,
80+
seeds = [b"player".as_ref(), signer.key().as_ref()],
81+
bump,
82+
)]
83+
pub player: Account<'info, PlayerData>,
84+
#[account(mut)]
85+
pub signer: Signer<'info>,
86+
pub system_program: Program<'info, System>,
87+
}
88+
89+
#[account]
90+
pub struct PlayerData {
91+
pub name: String,
92+
pub level: u8,
93+
pub xp: u64,
94+
pub wood: u64,
95+
pub energy: u64,
96+
pub last_login: i64
97+
}
98+
```
99+
100+
### Choping trees
101+
102+
Then whenever the player calls the chop_tree instruction we will check if the player has enough energy and reward him with one wood.
103+
104+
```rust
105+
#[error_code]
106+
pub enum ErrorCode {
107+
#[msg("Not enough energy")]
108+
NotEnoughEnergy,
109+
}
110+
111+
pub fn chop_tree(mut ctx: Context<ChopTree>) -> Result<()> {
112+
let account = &mut ctx.accounts;
113+
update_energy(account)?;
114+
115+
if ctx.accounts.player.energy == 0 {
116+
return err!(ErrorCode::NotEnoughEnergy);
117+
}
118+
119+
ctx.accounts.player.wood = ctx.accounts.player.wood + 1;
120+
ctx.accounts.player.energy = ctx.accounts.player.energy - 1;
121+
msg!("You chopped a tree and got 1 log. You have {} wood and {} energy left.", ctx.accounts.player.wood, ctx.accounts.player.energy);
122+
Ok(())
123+
}
124+
```
125+
126+
### Calculating the energy
127+
128+
The interesting part happens in the update_energy function. We check how much time has passed and calculate the energy that the player will have at the given time.
129+
The same thing we will also do in the client. So we basically lazily update the energy instead of polling it all the time.
130+
The is a common technic in game development.
131+
132+
```rust
133+
134+
const TIME_TO_REFILL_ENERGY: i64 = 60;
135+
const MAX_ENERGY: u64 = 10;
136+
137+
pub fn update_energy(ctx: &mut ChopTree) -> Result<()> {
138+
let mut time_passed: i64 = &Clock::get()?.unix_timestamp - &ctx.player.last_login;
139+
let mut time_spent: i64 = 0;
140+
while time_passed > TIME_TO_REFILL_ENERGY {
141+
ctx.player.energy = ctx.player.energy + 1;
142+
time_passed -= TIME_TO_REFILL_ENERGY;
143+
time_spent += TIME_TO_REFILL_ENERGY;
144+
if ctx.player.energy == MAX_ENERGY {
145+
break;
146+
}
147+
}
148+
149+
if ctx.player.energy >= MAX_ENERGY {
150+
ctx.player.last_login = Clock::get()?.unix_timestamp;
151+
} else {
152+
ctx.player.last_login += time_spent;
153+
}
154+
155+
Ok(())
156+
}
157+
```
158+
159+
## Js client
160+
161+
### Subscribe to account updates
162+
163+
It is possible to subscribe to account updates via a websocket. This get updates to this account pushed directly back to the client without the need to poll this data. This allows fast gameplay because the updates usually arrive after around 500ms.
164+
165+
```js
166+
useEffect(() => {
167+
if (!publicKey) {return;}
168+
const [pda] = PublicKey.findProgramAddressSync(
169+
[Buffer.from("player", "utf8"),
170+
publicKey.toBuffer()],
171+
new PublicKey(LUMBERJACK_PROGRAM_ID)
172+
);
173+
try {
174+
program.account.playerData.fetch(pda).then((data) => {
175+
setGameState(data);
176+
});
177+
} catch (e) {
178+
window.alert("No player data found, please init!");
179+
}
180+
181+
connection.onAccountChange(pda, (account) => {
182+
setGameState(program.coder.accounts.decode("playerData", account.data));
183+
});
184+
185+
}, [publicKey]);
186+
```
187+
188+
### Calculate energy and show countdown
189+
190+
In the java script client we can then perform the same logic and show a countdown timer for the player so that he knows when the next energy will be available:
191+
192+
```js
193+
useEffect(() => {
194+
const interval = setInterval(async () => {
195+
if (gameState == null || gameState.lastLogin == undefined || gameState.energy >= 10) {return;}
196+
const lastLoginTime=gameState.lastLogin * 1000;
197+
let timePassed = ((Date.now() - lastLoginTime) / 1000);
198+
while (timePassed > TIME_TO_REFILL_ENERGY && gameState.energy < MAX_ENERGY) {
199+
gameState.energy = (parseInt(gameState.energy) + 1);
200+
gameState.lastLogin = parseInt(gameState.lastLogin) + TIME_TO_REFILL_ENERGY;
201+
timePassed -= TIME_TO_REFILL_ENERGY;
202+
}
203+
setTimePassed(timePassed);
204+
let nextEnergyIn = Math.floor(TIME_TO_REFILL_ENERGY -timePassed);
205+
if (nextEnergyIn < TIME_TO_REFILL_ENERGY && nextEnergyIn > 0) {
206+
setEnergyNextIn(nextEnergyIn);
207+
} else {
208+
setEnergyNextIn(0);
209+
}
210+
211+
}, 1000);
212+
213+
return () => clearInterval(interval);
214+
}, [gameState, timePassed]);
215+
216+
...
217+
218+
{(gameState && <div className="flex flex-col items-center">
219+
{("Wood: " + gameState.wood + " Energy: " + gameState.energy + " Next energy in: " + nextEnergyIn )}
220+
</div>)}
221+
222+
```
223+
224+
### Session keys
225+
226+
Session keys is an optional component. What it does is creating a local key pair which is toped up with some sol which can be used to autoapprove transactions. The session token is only allowed on certain functions of the program and has an expiry of 23 hours. Then the player will get the sol back and can create a new session.
227+
228+
With this you can now build any energy based game and even if someone builds a bot for the game the most he can do is play optimally, which maybe even easier to achieve when playing normally depending on the logic of your game.
229+
230+
This game becomes even better when combined with the Token example from Solana Cookbook and you actually drop some spl token to the players.
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"
3+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2+
3+
## Getting Started
4+
5+
First, run the development server:
6+
7+
```bash
8+
npm run dev
9+
# or
10+
yarn dev
11+
# or
12+
pnpm dev
13+
```
14+
15+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16+
17+
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
18+
19+
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
20+
21+
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
22+
23+
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
24+
25+
## Learn More
26+
27+
To learn more about Next.js, take a look at the following resources:
28+
29+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
30+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
31+
32+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
33+
34+
## Deploy on Vercel
35+
36+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
37+
38+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

0 commit comments

Comments
 (0)