Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tiny adventure two #85

Merged
merged 12 commits into from
Nov 27, 2022
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/TinyAdventureTwo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Tutorial } from "../../components/Tutorial";

const TinyAdventureTwo = () => (
<Tutorial
// About section that will be shown under the description of the tutorial page
about={require("./about.md")}
// Actual tutorial pages to show next to the editor
pages={[
{ content: require("./pages/1.md") },
{ content: require("./pages/2.md") },
{ content: require("./pages/3.md") },
]}
// Initial files to have at the beginning of the tutorial
files={[
["src/lib.rs", require("./files/lib.rs")],
["client/client.ts", require("./files/client.ts.raw")],
["tests/index.test.ts", require("./files/anchor.test.ts.raw")],
]}
/>
);

export default TinyAdventureTwo;
9 changes: 9 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/about.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## About

This is the second part of the [Tiny Adventure Tutorial](/tutorials/tiny-adventure)

## What you will learn

- How to work with PDAs
- How to do cross program invocations(CPIs)
- How to use a PDA as a vault to pay rewards(SOL) to your players
95 changes: 95 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/files/anchor.test.ts.raw
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// No imports needed: web3, anchor, pg and more are globally available

// The PDA adress everyone will be able to control the character if the interact with your program
const [globalLevel1GameDataAccount, bump] =
await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("level1", "utf8")],
//[pg.wallet.publicKey.toBuffer()], <- You could also add the player wallet as a seed, then you would have one instance per player. Need to also change the seed in the rust part
pg.program.programId
);

// This is where the program will save the sol reward for the chests and from which the reward will be payed out again
const [chestVaultAccount, chestBump] =
await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("chestVault", "utf8")],
pg.program.programId
);

const CHEST_REWARD = 100000000;
const TRANSACTION_COST = 5000;

describe("Test", () => {
it("Initlialize", async () => {
// Initialize level set the player position back to 0 and the caller needs to pay to fill up the chest with sol

let txHash = await pg.program.methods
.initializeLevelOne()
.accounts({
chestVault: chestVaultAccount,
newGameDataAccount: globalLevel1GameDataAccount,
signer: pg.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
})
.signers([pg.wallet.keypair])
.rpc();

console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
await pg.connection.confirmTransaction(txHash);
});

it("SpawningChestCostsSol", async () => {
let balanceBefore = await pg.connection.getBalance(pg.wallet.publicKey);
console.log(`My balance before spawning a chest: ${balanceBefore} SOL`);

let txHash = await pg.program.methods
.resetLevelAndSpawnChest()
.accounts({
chestVault: chestVaultAccount,
gameDataAccount: globalLevel1GameDataAccount,
payer: pg.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
})
.signers([pg.wallet.keypair])
.rpc();

await pg.connection.confirmTransaction(txHash);

let balanceAfter = await pg.connection.getBalance(pg.wallet.publicKey);
console.log(`My balance after spawning a chest: ${balanceAfter} SOL`);

assert(balanceBefore - CHEST_REWARD - TRANSACTION_COST == balanceAfter);
});

it("Move to the right and collect chest", async () => {
let gameDateAccount;
let balanceBefore = await pg.connection.getBalance(pg.wallet.publicKey);

// Here we move to the right three times and collect the chest at the end of the level
for (let i = 0; i < 3; i++) {
let txHash = await pg.program.methods
.moveRight()
.accounts({
chestVault: chestVaultAccount,
gameDataAccount: globalLevel1GameDataAccount,
systemProgram: web3.SystemProgram.programId,
player: pg.wallet.publicKey,
})
.signers([pg.wallet.keypair])
.rpc();

await pg.connection.confirmTransaction(txHash);

gameDateAccount = await pg.program.account.gameDataAccount.fetch(
globalLevel1GameDataAccount
);
}

let balanceAfter = await pg.connection.getBalance(pg.wallet.publicKey);

console.log(
`Balance before collecting chest: ${balanceBefore} Balance after collecting chest: ${balanceAfter}`
);
assert(balanceBefore + CHEST_REWARD - 3 * TRANSACTION_COST == balanceAfter);
assert(gameDateAccount.playerPosition == 3);
});
});
93 changes: 93 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/files/client.ts.raw
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// The PDA adress everyone will be able to control the character if the interact with your program
const [globalLevel1GameDataAccount, bump] =
await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("level1", "utf8")],
//[pg.wallet.publicKey.toBuffer()], <- You could also add the player wallet as a seed, then you would have one instance per player. Need to also change the seed in the rust part
pg.program.programId
);

// This is where the program will save the sol reward for the chests and from which the reward will be payed out again
const [chestVaultAccount, chestBump] =
await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("chestVault", "utf8")],
pg.program.programId
);

// Initialize level set the player position back to 0 and the caller needs to pay to fill up the chest with sol
let txHash = await pg.program.methods
.initializeLevelOne()
.accounts({
chestVault: chestVaultAccount,
newGameDataAccount: globalLevel1GameDataAccount,
signer: pg.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
})
.signers([pg.wallet.keypair])
.rpc();

console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
await pg.connection.confirmTransaction(txHash);

let balance = await pg.connection.getBalance(pg.wallet.publicKey);
console.log(
`My balance before spawning a chest: ${balance / web3.LAMPORTS_PER_SOL} SOL`
);

txHash = await pg.program.methods
.resetLevelAndSpawnChest()
.accounts({
chestVault: chestVaultAccount,
gameDataAccount: globalLevel1GameDataAccount,
payer: pg.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
})
.signers([pg.wallet.keypair])
.rpc();

console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
await pg.connection.confirmTransaction(txHash);

console.log("Level reset and chest spawned 💎");
console.log("o........💎");

// Here we move to the right three times and collect the chest at the end of the level
for (let i = 0; i < 3; i++) {
txHash = await pg.program.methods
.moveRight()
.accounts({
chestVault: chestVaultAccount,
gameDataAccount: globalLevel1GameDataAccount,
systemProgram: web3.SystemProgram.programId,
player: pg.wallet.publicKey,
})
.signers([pg.wallet.keypair])
.rpc();

console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
await pg.connection.confirmTransaction(txHash);
let balance = await pg.connection.getBalance(pg.wallet.publicKey);
console.log(`My balance: ${balance / web3.LAMPORTS_PER_SOL} SOL`);

let gameDateAccount = await pg.program.account.gameDataAccount.fetch(
globalLevel1GameDataAccount
);

console.log("Player position is:", gameDateAccount.playerPosition.toString());

switch (gameDateAccount.playerPosition) {
case 0:
console.log("A journey begins...");
console.log("o........💎");
break;
case 1:
console.log("....o....💎");
break;
case 2:
console.log("......o..💎");
break;
case 3:
console.log(".........\\o/💎");
console.log("...........\\o/");
break;
}
}
144 changes: 144 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/files/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
use anchor_lang::prelude::*;
use anchor_lang::solana_program::native_token::LAMPORTS_PER_SOL;
use anchor_lang::system_program;

// This is your program's public key and it will update
// automatically when you build the project.
declare_id!("");

#[program]
mod tiny_adventure_two {
use super::*;

// The amount of lamports that will be put into chests and given out as rewards.
const CHEST_REWARD: u64 = LAMPORTS_PER_SOL / 10; // 0.1 SOL

pub fn initialize_level_one(_ctx: Context<InitializeLevelOne>) -> Result<()> {
// Usually in your production code you would not print lots of text because it cost compute units.
msg!("A Journey Begins!");
msg!("o.......💎");
Ok(())
}

// this will the the player position of the given level back to 0 and fill up the chest with sol
pub fn reset_level_and_spawn_chest(ctx: Context<SpawnChest>) -> Result<()> {
ctx.accounts.game_data_account.player_position = 0;

let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info().clone(),
to: ctx.accounts.chest_vault.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, CHEST_REWARD)?;

msg!("Level Reset and Chest Spawned at position 3");

Ok(())
}

pub fn move_right(ctx: Context<MoveRight>) -> Result<()> {
let game_data_account = &mut ctx.accounts.game_data_account;
if game_data_account.player_position == 3 {
msg!("You have reached the end! Super!");
} else if game_data_account.player_position == 2 {
game_data_account.player_position = game_data_account.player_position + 1;

msg!(
"You made it! Here is your reward {0} lamports",
CHEST_REWARD
);

**ctx
.accounts
.chest_vault
.to_account_info()
.try_borrow_mut_lamports()? -= CHEST_REWARD;
**ctx
.accounts
.player
.to_account_info()
.try_borrow_mut_lamports()? += CHEST_REWARD;
} else {
game_data_account.player_position = game_data_account.player_position + 1;
print_player(game_data_account.player_position);
}
Ok(())
}
}

fn print_player(player_position: u8) {
if player_position == 0 {
msg!("A Journey Begins!");
msg!("o.........💎");
} else if player_position == 1 {
msg!("..o.......💎");
} else if player_position == 2 {
msg!("....o.....💎");
} else if player_position == 3 {
msg!("........\\o/💎");
msg!("..........\\o/");
msg!("You have reached the end! Super!");
}
}

#[derive(Accounts)]
pub struct InitializeLevelOne<'info> {
// We must specify the space in order to initialize an account.
// First 8 bytes are default account discriminator,
// next 1 byte come from NewAccount.data being type u8.
// (u8 = 8 bits unsigned integer = 8 bytes)
// You can also use the signer as seed [signer.key().as_ref()],
#[account(
init_if_needed,
seeds = [b"level1"],
bump,
payer = signer,
space = 8 + 1
)]
pub new_game_data_account: Account<'info, GameDataAccount>,
// This is the PDA in which we will deposit the reward SOl and
// from where we send it back to the first player reaching the chest.
#[account(
init_if_needed,
seeds = [b"chestVault"],
bump,
payer = signer,
space = 8
)]
pub chest_vault: Account<'info, ChestVaultAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct SpawnChest<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(mut, seeds = [b"chestVault"], bump)]
pub chest_vault: Account<'info, ChestVaultAccount>,
#[account(mut)]
pub game_data_account: Account<'info, GameDataAccount>,
pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct MoveRight<'info> {
#[account(mut, seeds = [b"chestVault"], bump)]
pub chest_vault: Account<'info, ChestVaultAccount>,
#[account(mut)]
pub game_data_account: Account<'info, GameDataAccount>,
#[account(mut)]
pub player: Signer<'info>,
pub system_program: Program<'info, System>,
}

#[account]
pub struct GameDataAccount {
player_position: u8,
}

#[account]
pub struct ChestVaultAccount {}
1 change: 1 addition & 0 deletions client/src/tutorials/TinyAdventureTwo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./TinyAdventureTwo";
13 changes: 13 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/pages/1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Rewarding players with SOL! 📝

Hey!

Please follow the instruction for the setup in the [Tiny Adventure](/tutorials/tiny-adventure) tutorial for the basics if you have not yet.

In this tutorial we will learn how we can deposit SOL in a chest and reward it to a player.

For that we will use a second PDA called chest vault.

![](/tutorials/tiny-adventure-two/tinyAdventureTwo.jpg)

In later tutorials I will show you how to connect to this program from within Unity game engine.
15 changes: 15 additions & 0 deletions client/src/tutorials/TinyAdventureTwo/pages/2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Instructions

For the basics please follow tutorial [Tiny Adventure](/tutorials/tiny-adventure) first.

1. In the terminal below type `build` to the project.
2. Connect your wallet from bottom left.
3. Save your keypair.
4. Get some SOL: `solana airdrop 2` (If you get this error:
Process error: Client error: "Too many requests for a specific RPC call, contact your app developer or support@rpcpool.com."
then pick another RPC Endpoint on the bottom left the little gear symbol. `devnet-alchemy` works well for me).
5. Run `solana airdrop 2` until you have 6 SOL.
6. You can see your balance with `solana balance`. You can also see your balance in the bottom bar.
7. Type `deploy` This will now deploy your game to devnet. This will take a while.

Now that the program is deployed to devnet you can continue to the next step, running the client.