diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de5659 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d29b859 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Dojo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..cf68d91 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,62 @@ + + + Dojo logo + + + + + + + + + +[![discord](https://img.shields.io/badge/join-dojo-green?logo=discord&logoColor=white)](https://discord.gg/PwDa2mKhR4) +[![Telegram Chat][tg-badge]][tg-url] + +[tg-badge]: https://img.shields.io/endpoint?color=neon&logo=telegram&label=chat&style=flat-square&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fdojoengine +[tg-url]: https://t.me/dojoengine + +# Dojo Starter: Official Guide + +The official Dojo Starter guide, the quickest and most streamlined way to get your Dojo provable game up and running. This guide will assist you with the initial setup, from cloning the repository to deploying your world. + +Read the full tutorial [here](https://book.dojoengine.org/tutorial/dojo-starter). + +## Running Locally + +#### Terminal one (Make sure this is running) +```bash +# Run Katana +katana --disable-fee --allowed-origins "*" +``` + +#### Terminal two +```bash +# Build the example +sozo build + +# Migrate the example +sozo migrate apply + +# Start Torii +torii --world 0x70835f8344647b1e573fe7aeccbf044230089eb19624d3c7dea4080f5dcb025 --allowed-origins "*" +``` + +--- + +## Contribution + +This starter project is a constant work in progress and contributions are greatly appreciated! + +1. **Report a Bug** + + - If you think you have encountered a bug, and we should know about it, feel free to report it [here](https://github.com/dojoengine/dojo-starter/issues) and we will take care of it. + +2. **Request a Feature** + + - You can also request for a feature [here](https://github.com/dojoengine/dojo-starter/issues), and if it's viable, it will be picked for development. + +3. **Create a Pull Request** + - It can't get better then this, your pull request will be appreciated by the community. + +Happy coding! diff --git a/contracts/Scarb.lock b/contracts/Scarb.lock new file mode 100644 index 0000000..a95d5b4 --- /dev/null +++ b/contracts/Scarb.lock @@ -0,0 +1,22 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "1.0.0-alpha.4" +source = "git+https://github.com/dojoengine/dojo?tag=v1.0.0-alpha.5#6878242e120d3135d3bc1bb94135d7135693069b" +dependencies = [ + "dojo_plugin", +] + +[[package]] +name = "dojo_plugin" +version = "1.0.0-alpha.4" +source = "git+https://github.com/dojoengine/dojo?rev=f15def33#f15def330c0d099e79351d11c197f63e8cc1ff36" + +[[package]] +name = "rpg" +version = "0.1.0" +dependencies = [ + "dojo", +] diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml new file mode 100644 index 0000000..921539c --- /dev/null +++ b/contracts/Scarb.toml @@ -0,0 +1,15 @@ +[package] +cairo-version = "=2.7.0" +name = "rpg" +version = "0.1.0" + +[cairo] +sierra-replace-ids = true + +[scripts] +migrate = "sozo build && sozo migrate apply" + +[dependencies] +dojo = { git = "https://github.com/dojoengine/dojo", tag = "v1.0.0-alpha.5" } + +[[target.dojo]] diff --git a/contracts/assets/cover.png b/contracts/assets/cover.png new file mode 100644 index 0000000..8ac043f Binary files /dev/null and b/contracts/assets/cover.png differ diff --git a/contracts/assets/icon.png b/contracts/assets/icon.png new file mode 100644 index 0000000..2169b56 Binary files /dev/null and b/contracts/assets/icon.png differ diff --git a/contracts/dojo_dev.toml b/contracts/dojo_dev.toml new file mode 100644 index 0000000..c192184 --- /dev/null +++ b/contracts/dojo_dev.toml @@ -0,0 +1,21 @@ +[world] +name = "Dojo starter RPG" +description = "A fully onchain RPG game." +cover_uri = "file://assets/cover.png" +icon_uri = "file://assets/icon.png" +website = "https://github.com/dojoengine/dojo-starter" +seed = "dojo_starter_rpg" + +[world.socials] +x = "https://x.com/ohayo_dojo" +discord = "https://discord.gg/FB2wR6uF" +github = "https://github.com/dojoengine/starter-rpg" +telegram = "https://t.me/dojoengine" + +[namespace] +default = "dojo_starter_rpg" + +[env] +rpc_url = "http://localhost:5050/" +account_address = "0xb3ff441a68610b30fd5e2abbf3a1548eb6ba6f3559f2862bf2dc757e5828ca" +private_key = "0x2bbf4f9fd0bbb2e60b0316c1fe0b76cf7a4d0198bd493ced9b8df2a3a24d68a" diff --git a/contracts/overlays/dev/actions.toml b/contracts/overlays/dev/actions.toml new file mode 100644 index 0000000..d75928e --- /dev/null +++ b/contracts/overlays/dev/actions.toml @@ -0,0 +1,2 @@ +tag = "dojo_starter-actions" +writes = ["dojo_starter_rpg-Player", "dojo_starter_rpg-Dungeon"] diff --git a/contracts/src/components/playable.cairo b/contracts/src/components/playable.cairo new file mode 100644 index 0000000..4f91a9c --- /dev/null +++ b/contracts/src/components/playable.cairo @@ -0,0 +1,133 @@ +#[starknet::component] +mod PlayableComponent { + // Core imports + + use core::debug::PrintTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::info::{get_caller_address, get_block_timestamp}; + + // Dojo imports + + use dojo::world::IWorldDispatcher; + use dojo::world::IWorldDispatcherTrait; + + // Internal imports + + use rpg::constants; + use rpg::store::{Store, StoreTrait}; + use rpg::models::player::{Player, PlayerTrait, PlayerAssert}; + use rpg::models::dungeon::{Dungeon, DungeonTrait, DungeonAssert}; + use rpg::types::role::Role; + use rpg::types::mode::Mode; + use rpg::types::direction::Direction; + + // Errors + + mod errors {} + + // Storage + + #[storage] + struct Storage {} + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event {} + + #[generate_trait] + impl InternalImpl< + TContractState, +HasComponent + > of InternalTrait { + fn spawn( + self: @ComponentState, + world: IWorldDispatcher, + name: felt252, + role: u8, + mode: u8 + ) { + // [Setup] Datastore + let store: Store = StoreTrait::new(world); + + // [Effect] Create player + let player_id: felt252 = get_caller_address().into(); + let time: u64 = get_block_timestamp(); + let mut player = PlayerTrait::new(player_id, name, time, mode.into()); + + // [Effect] Player role + player.enrole(role.into()); + + // [Effect] Set player + store.set_player(player); + } + + fn move(self: @ComponentState, world: IWorldDispatcher, direction: u8) { + // [Setup] Datastore + let store: Store = StoreTrait::new(world); + let player_id: felt252 = get_caller_address().into(); + let (mut player, dungeon) = store.get_state(player_id); + + // [Check] Player is not dead + player.assert_not_dead(); + + // [Check] Current dungeon is done + dungeon.assert_is_done(); + + // [Effect] Move player + let (monster, role) = player.move(direction.into()); + let new_dungeon: Dungeon = DungeonTrait::new(dungeon.id, monster, role); + + // [Effect] Update state + store.set_state(player, new_dungeon); + } + + fn attack(self: @ComponentState, world: IWorldDispatcher) { + // [Setup] Datastore + let store: Store = StoreTrait::new(world); + let player_id: felt252 = get_caller_address().into(); + let (mut player, mut dungeon) = store.get_state(player_id); + + // [Check] Player is not dead + player.assert_not_dead(); + + // [Check] Current dungeon is not done + dungeon.assert_not_done(); + + // [Effect] Attack + dungeon.take_damage(player.role.into(), player.damage); + + // [Effect] Defend + if dungeon.is_done() { + player.reward(dungeon.treasury()); + } else { + player.take_damage(dungeon.role.into(), dungeon.damage); + } + + // [Effect] Update state + store.set_state(player, dungeon); + } + + fn heal(self: @ComponentState, world: IWorldDispatcher, quantity: u8) { + // [Setup] Datastore + let store: Store = StoreTrait::new(world); + let player_id: felt252 = get_caller_address().into(); + let (mut player, dungeon) = store.get_state(player_id); + + // [Check] Player is not dead + player.assert_not_dead(); + + // [Check] Current dungeon is a shop + dungeon.assert_is_shop(); + + // [Effect] Heal + player.heal(quantity); + + // [Effect] Update state + store.set_player(player); + } + } +} diff --git a/contracts/src/constants.cairo b/contracts/src/constants.cairo new file mode 100644 index 0000000..48975a0 --- /dev/null +++ b/contracts/src/constants.cairo @@ -0,0 +1,16 @@ +// Game + +const SEED_WEEK_SECONDS: u64 = 604800; +const SEED_OFFSET_SECONDS: u64 = 345600; + +// Player + +const DEFAULT_PLAYER_DAMAGE: u8 = 10; +const DEFAULT_PLAYER_HEALTH: u8 = 100; +const MAX_PLAYER_HEALTH: u8 = 200; +const DEFAULT_PLAYER_GOLD: u16 = 0; + +// Shop + +const DEFAULT_POTION_COST: u16 = 10; +const DEFAULT_POTION_HEAL: u8 = 20; diff --git a/contracts/src/elements/modes/easy.cairo b/contracts/src/elements/modes/easy.cairo new file mode 100644 index 0000000..e31842d --- /dev/null +++ b/contracts/src/elements/modes/easy.cairo @@ -0,0 +1,35 @@ +// Internal imports + +use rpg::elements::modes::interface::{ModeTrait, Monster, Role, RoleTrait}; + +impl Easy of ModeTrait { + #[inline] + fn monster(seed: felt252) -> Monster { + let luck: u256 = seed.into() % 100; + if luck < 20 { + Monster::None + } else if luck < 25 { + Monster::Boss + } else if luck < 35 { + Monster::Elite + } else { + Monster::Common + } + } + + #[inline] + fn role(seed: felt252, player_role: Role) -> Role { + let luck: u256 = seed.into() % 100; + if luck < 20 { + Role::Fire + } else if luck < 40 { + Role::Water + } else if luck < 60 { + Role::Earth + } else if luck < 80 { + Role::Air + } else { + player_role.strength() + } + } +} diff --git a/contracts/src/elements/modes/hard.cairo b/contracts/src/elements/modes/hard.cairo new file mode 100644 index 0000000..025d517 --- /dev/null +++ b/contracts/src/elements/modes/hard.cairo @@ -0,0 +1,35 @@ +// Internal imports + +use rpg::elements::modes::interface::{ModeTrait, Monster, Role, RoleTrait}; + +impl Hard of ModeTrait { + #[inline] + fn monster(seed: felt252) -> Monster { + let luck: u256 = seed.into() % 100; + if luck < 5 { + Monster::None + } else if luck < 25 { + Monster::Boss + } else if luck < 60 { + Monster::Elite + } else { + Monster::Common + } + } + + #[inline] + fn role(seed: felt252, player_role: Role) -> Role { + let luck: u256 = seed.into() % 100; + if luck < 20 { + Role::Fire + } else if luck < 40 { + Role::Water + } else if luck < 60 { + Role::Earth + } else if luck < 80 { + Role::Air + } else { + player_role.weakness() + } + } +} diff --git a/contracts/src/elements/modes/interface.cairo b/contracts/src/elements/modes/interface.cairo new file mode 100644 index 0000000..32e38f9 --- /dev/null +++ b/contracts/src/elements/modes/interface.cairo @@ -0,0 +1,9 @@ +// Internal imports + +use rpg::types::monster::Monster; +use rpg::types::role::{Role, RoleTrait}; + +trait ModeTrait { + fn monster(seed: felt252) -> Monster; + fn role(seed: felt252, player_role: Role) -> Role; +} diff --git a/contracts/src/elements/modes/medium.cairo b/contracts/src/elements/modes/medium.cairo new file mode 100644 index 0000000..d4161da --- /dev/null +++ b/contracts/src/elements/modes/medium.cairo @@ -0,0 +1,33 @@ +// Internal imports + +use rpg::elements::modes::interface::{ModeTrait, Monster, Role}; + +impl Medium of ModeTrait { + #[inline] + fn monster(seed: felt252) -> Monster { + let luck: u256 = seed.into() % 100; + if luck < 10 { + Monster::None + } else if luck < 20 { + Monster::Boss + } else if luck < 45 { + Monster::Elite + } else { + Monster::Common + } + } + + #[inline] + fn role(seed: felt252, player_role: Role) -> Role { + let luck: u256 = seed.into() % 100; + if luck < 25 { + Role::Fire + } else if luck < 50 { + Role::Water + } else if luck < 75 { + Role::Earth + } else { + Role::Air + } + } +} diff --git a/contracts/src/elements/monsters/boss.cairo b/contracts/src/elements/monsters/boss.cairo new file mode 100644 index 0000000..ab3491f --- /dev/null +++ b/contracts/src/elements/monsters/boss.cairo @@ -0,0 +1,26 @@ +// Internal imports + +use rpg::elements::monsters::interface::MonsterTrait; + +// Constants + +const DAMAGE: u8 = 25; +const HEALTH: u8 = 50; +const REWARD: u16 = 35; + +impl Boss of MonsterTrait { + #[inline] + fn damage() -> u8 { + DAMAGE + } + + #[inline] + fn health() -> u8 { + HEALTH + } + + #[inline] + fn reward() -> u16 { + REWARD + } +} diff --git a/contracts/src/elements/monsters/common.cairo b/contracts/src/elements/monsters/common.cairo new file mode 100644 index 0000000..c0d1123 --- /dev/null +++ b/contracts/src/elements/monsters/common.cairo @@ -0,0 +1,26 @@ +// Internal imports + +use rpg::elements::monsters::interface::MonsterTrait; + +// Constants + +const DAMAGE: u8 = 10; +const HEALTH: u8 = 20; +const REWARD: u16 = 10; + +impl Common of MonsterTrait { + #[inline] + fn damage() -> u8 { + DAMAGE + } + + #[inline] + fn health() -> u8 { + HEALTH + } + + #[inline] + fn reward() -> u16 { + REWARD + } +} diff --git a/contracts/src/elements/monsters/elite.cairo b/contracts/src/elements/monsters/elite.cairo new file mode 100644 index 0000000..d690f36 --- /dev/null +++ b/contracts/src/elements/monsters/elite.cairo @@ -0,0 +1,26 @@ +// Internal imports + +use rpg::elements::monsters::interface::MonsterTrait; + +// Constants + +const DAMAGE: u8 = 15; +const HEALTH: u8 = 30; +const REWARD: u16 = 15; + +impl Elite of MonsterTrait { + #[inline] + fn damage() -> u8 { + DAMAGE + } + + #[inline] + fn health() -> u8 { + HEALTH + } + + #[inline] + fn reward() -> u16 { + REWARD + } +} diff --git a/contracts/src/elements/monsters/interface.cairo b/contracts/src/elements/monsters/interface.cairo new file mode 100644 index 0000000..4f7d492 --- /dev/null +++ b/contracts/src/elements/monsters/interface.cairo @@ -0,0 +1,5 @@ +trait MonsterTrait { + fn damage() -> u8; + fn health() -> u8; + fn reward() -> u16; +} diff --git a/contracts/src/elements/roles/air.cairo b/contracts/src/elements/roles/air.cairo new file mode 100644 index 0000000..31882b8 --- /dev/null +++ b/contracts/src/elements/roles/air.cairo @@ -0,0 +1,15 @@ +// Internal imports + +use rpg::elements::roles::interface::{Role, RoleTrait}; + +impl Air of RoleTrait { + #[inline] + fn weakness(role: Role) -> Role { + Role::Fire + } + + #[inline] + fn strength(role: Role) -> Role { + Role::Earth + } +} diff --git a/contracts/src/elements/roles/earth.cairo b/contracts/src/elements/roles/earth.cairo new file mode 100644 index 0000000..2ed6320 --- /dev/null +++ b/contracts/src/elements/roles/earth.cairo @@ -0,0 +1,15 @@ +// Internal imports + +use rpg::elements::roles::interface::{Role, RoleTrait}; + +impl Earth of RoleTrait { + #[inline] + fn weakness(role: Role) -> Role { + Role::Air + } + + #[inline] + fn strength(role: Role) -> Role { + Role::Water + } +} diff --git a/contracts/src/elements/roles/fire.cairo b/contracts/src/elements/roles/fire.cairo new file mode 100644 index 0000000..5bb7e77 --- /dev/null +++ b/contracts/src/elements/roles/fire.cairo @@ -0,0 +1,15 @@ +// Internal imports + +use rpg::elements::roles::interface::{Role, RoleTrait}; + +impl Fire of RoleTrait { + #[inline] + fn weakness(role: Role) -> Role { + Role::Water + } + + #[inline] + fn strength(role: Role) -> Role { + Role::Air + } +} diff --git a/contracts/src/elements/roles/interface.cairo b/contracts/src/elements/roles/interface.cairo new file mode 100644 index 0000000..61c9a83 --- /dev/null +++ b/contracts/src/elements/roles/interface.cairo @@ -0,0 +1,8 @@ +// Internal imports + +use rpg::types::role::Role; + +trait RoleTrait { + fn weakness(role: Role) -> Role; + fn strength(role: Role) -> Role; +} diff --git a/contracts/src/elements/roles/water.cairo b/contracts/src/elements/roles/water.cairo new file mode 100644 index 0000000..f6e963f --- /dev/null +++ b/contracts/src/elements/roles/water.cairo @@ -0,0 +1,15 @@ +// Internal imports + +use rpg::elements::roles::interface::{Role, RoleTrait}; + +impl Water of RoleTrait { + #[inline] + fn weakness(role: Role) -> Role { + Role::Earth + } + + #[inline] + fn strength(role: Role) -> Role { + Role::Fire + } +} diff --git a/contracts/src/helpers/seeder.cairo b/contracts/src/helpers/seeder.cairo new file mode 100644 index 0000000..0c1e1d6 --- /dev/null +++ b/contracts/src/helpers/seeder.cairo @@ -0,0 +1,24 @@ +// Core imports + +use core::poseidon::{PoseidonTrait, HashState}; +use core::hash::HashStateTrait; + +// Internal imports + +use rpg::constants::{SEED_WEEK_SECONDS, SEED_OFFSET_SECONDS}; + +#[generate_trait] +impl Seeder of SeederTrait { + #[inline] + fn reseed(lhs: felt252, rhs: felt252) -> felt252 { + let state: HashState = PoseidonTrait::new(); + let state = state.update(lhs); + let state = state.update(rhs); + state.finalize() + } + + #[inline] + fn compute_id(time: u64) -> u64 { + (time + SEED_OFFSET_SECONDS) / SEED_WEEK_SECONDS + } +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo new file mode 100644 index 0000000..23e936a --- /dev/null +++ b/contracts/src/lib.cairo @@ -0,0 +1,58 @@ +mod constants; +mod store; + +mod types { + mod direction; + mod mode; + mod monster; + mod role; +} + +mod models { + mod index; + mod dungeon; + mod player; +} + +mod components { + mod playable; +} + +mod systems { + mod actions; +} + +mod elements { + mod modes { + mod interface; + mod easy; + mod medium; + mod hard; + } + mod monsters { + mod interface; + mod common; + mod elite; + mod boss; + } + mod roles { + mod interface; + mod fire; + mod water; + mod earth; + mod air; + } +} + +mod helpers { + mod seeder; +} + +#[cfg(test)] +mod tests { + mod setup; + mod test_setup; + mod test_move; + mod test_attack; + mod test_heal; +} diff --git a/contracts/src/models/dungeon.cairo b/contracts/src/models/dungeon.cairo new file mode 100644 index 0000000..cc174f6 --- /dev/null +++ b/contracts/src/models/dungeon.cairo @@ -0,0 +1,88 @@ +// Core imports + +use core::debug::PrintTrait; +use core::poseidon::{PoseidonTrait, HashState}; +use core::hash::HashStateTrait; + +// Inernal imports + +use rpg::constants; +use rpg::models::index::Dungeon; +use rpg::types::mode::{Mode, ModeTrait}; +use rpg::types::role::{Role, RoleTrait}; +use rpg::types::monster::{Monster, MonsterTrait}; + +mod errors { + const DUNGEON_NOT_DONE: felt252 = 'Dungeon: not done'; + const DUNGEON_ALREADY_DONE: felt252 = 'Dungeon: already done'; + const DUNGEON_NOT_SHOP: felt252 = 'Dungeon: not shop'; +} + +#[generate_trait] +impl DungeonImpl of DungeonTrait { + #[inline] + fn new(id: felt252, monster: Monster, role: Role) -> Dungeon { + Dungeon { + id, + monster: monster.into(), + role: role.into(), + damage: monster.damage(), + health: monster.health(), + reward: monster.reward(), + } + } + + #[inline] + fn is_done(self: Dungeon) -> bool { + self.monster == Monster::None.into() || self.health == 0 + } + + #[inline] + fn take_damage(ref self: Dungeon, player_role: Role, damage: u8) { + let monster_role: Role = self.role.into(); + let received_damage = monster_role.received_damage(player_role, damage); + self.health -= core::cmp::min(self.health, received_damage); + } + + #[inline] + fn treasury(self: Dungeon) -> u16 { + let monster: Monster = self.monster.into(); + monster.reward() + } +} + +#[generate_trait] +impl DungeonAssert of AssertTrait { + #[inline] + fn assert_is_done(self: Dungeon) { + assert(self.is_zero(), errors::DUNGEON_NOT_DONE); + } + + #[inline] + fn assert_not_done(self: Dungeon) { + assert(self.is_non_zero(), errors::DUNGEON_ALREADY_DONE); + } + + #[inline] + fn assert_is_shop(self: Dungeon) { + assert(self.monster == Monster::None.into(), errors::DUNGEON_NOT_SHOP); + } +} + +impl ZeroableDungeonImpl of core::Zeroable { + #[inline] + fn zero() -> Dungeon { + Dungeon { id: 0, monster: 0, role: 0, damage: 0, health: 0, reward: 0 } + } + + #[inline] + fn is_zero(self: Dungeon) -> bool { + self.monster == Monster::None.into() || self.health == 0 + } + + #[inline] + fn is_non_zero(self: Dungeon) -> bool { + !self.is_zero() + } +} + diff --git a/contracts/src/models/index.cairo b/contracts/src/models/index.cairo new file mode 100644 index 0000000..fb98ad0 --- /dev/null +++ b/contracts/src/models/index.cairo @@ -0,0 +1,26 @@ +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Player { + #[key] + pub id: felt252, + pub mode: u8, + pub role: u8, + pub damage: u8, + pub health: u8, + pub gold: u16, + pub score: u16, + pub seed: felt252, + pub name: felt252, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct Dungeon { + #[key] + pub id: felt252, + pub monster: u8, + pub role: u8, + pub damage: u8, + pub health: u8, + pub reward: u16, +} diff --git a/contracts/src/models/player.cairo b/contracts/src/models/player.cairo new file mode 100644 index 0000000..c3af454 --- /dev/null +++ b/contracts/src/models/player.cairo @@ -0,0 +1,149 @@ +// Core imports + +use core::debug::PrintTrait; + +// Inernal imports + +use rpg::constants::{ + DEFAULT_POTION_HEAL, MAX_PLAYER_HEALTH, DEFAULT_POTION_COST, DEFAULT_PLAYER_DAMAGE, + DEFAULT_PLAYER_HEALTH, DEFAULT_PLAYER_GOLD +}; +use rpg::models::index::Player; +use rpg::types::role::{Role, RoleTrait}; +use rpg::types::direction::Direction; +use rpg::types::mode::{Mode, ModeTrait}; +use rpg::types::monster::{Monster, MonsterTrait}; +use rpg::helpers::seeder::Seeder; + +mod errors { + const PLAYER_NOT_EXIST: felt252 = 'Player: does not exist'; + const PLAYER_ALREADY_EXIST: felt252 = 'Player: already exist'; + const PLAYER_INVALID_NAME: felt252 = 'Player: invalid name'; + const PLAYER_INVALID_CLASS: felt252 = 'Player: invalid role'; + const PLAYER_INVALID_DIRECTION: felt252 = 'Player: invalid direction'; + const PLAYER_NOT_ENOUGH_GOLD: felt252 = 'Player: not enough gold'; + const PLAYER_IS_DEAD: felt252 = 'Player: is dead'; +} + +#[generate_trait] +impl PlayerImpl of PlayerTrait { + #[inline] + fn new(id: felt252, name: felt252, time: u64, mode: Mode) -> Player { + // [Check] Name is valid + assert(name != 0, errors::PLAYER_INVALID_NAME); + // [Compute] Weekly seed according to the timestamp and the mode + let seed_id: felt252 = Seeder::compute_id(time).into(); + let seed = Seeder::reseed(seed_id, mode.into()); + // [Return] Player + Player { + id, + mode: mode.into(), + role: Role::None.into(), + damage: DEFAULT_PLAYER_DAMAGE, + health: DEFAULT_PLAYER_HEALTH, + gold: DEFAULT_PLAYER_GOLD, + score: 0, + seed, + name + } + } + + #[inline] + fn enrole(ref self: Player, role: Role) { + // [Check] Role is valid + let role_id: u8 = role.into(); + assert(role_id != Role::None.into(), errors::PLAYER_INVALID_CLASS); + // [Effect] Change the role + self.role = role_id; + // [Effect] Reseed + self.seed = Seeder::reseed(self.seed, role_id.into()); + } + + #[inline] + fn move(ref self: Player, direction: Direction) -> (Monster, Role) { + // [Check] Direction is valid + let direction_id: u8 = direction.into(); + assert(direction_id != Direction::None.into(), errors::PLAYER_INVALID_DIRECTION); + // [Effect] For the first move, spawn a specific monster and a role + if self.score == 0 { + let role: Role = self.role.into(); + return (Monster::Common, role.strength()); + } + // [Effect] Spawn monster and role + let seed: u256 = self.seed.into(); + let mode: Mode = self.mode.into(); + let monster: Monster = mode.monster(seed.low.into()); + let role: Role = mode.role(seed.high.into(), self.role.into()); + // [Effect] Reseed + self.seed = Seeder::reseed(self.seed, direction_id.into()); + // [Return] Monster and role + (monster, role) + } + + #[inline] + fn take_damage(ref self: Player, monster_role: Role, damage: u8) { + let player_role: Role = self.role.into(); + let received_damage = player_role.received_damage(monster_role, damage); + self.health -= core::cmp::min(self.health, received_damage); + } + + #[inline] + fn reward(ref self: Player, gold: u16) { + self.gold += gold; + self.score += 1; + } + + #[inline] + fn heal(ref self: Player, quantity: u8) { + // [Check] Affordable + let cost: u16 = quantity.into() * DEFAULT_POTION_COST; + self.assert_is_affordable(cost); + // [Effect] Remove gold + self.gold -= cost; + // [Effect] Restore health + self.health += core::cmp::min(DEFAULT_POTION_HEAL, MAX_PLAYER_HEALTH - DEFAULT_POTION_HEAL); + } +} + +#[generate_trait] +impl PlayerAssert of AssertTrait { + #[inline] + fn assert_exists(self: Player) { + assert(self.is_non_zero(), errors::PLAYER_NOT_EXIST); + } + + #[inline] + fn assert_not_exists(self: Player) { + assert(self.is_zero(), errors::PLAYER_ALREADY_EXIST); + } + + #[inline] + fn assert_not_dead(self: Player) { + assert(self.health != 0, errors::PLAYER_IS_DEAD); + } + + #[inline] + fn assert_is_affordable(self: Player, cost: u16) { + assert(self.gold >= cost, errors::PLAYER_NOT_ENOUGH_GOLD); + } +} + +impl ZeroablePlayerImpl of core::Zeroable { + #[inline] + fn zero() -> Player { + Player { + id: 0, mode: 0, role: 0, damage: 0, health: 0, gold: 0, score: 0, seed: 0, name: 0 + } + } + + #[inline] + fn is_zero(self: Player) -> bool { + 0 == self.name + } + + #[inline] + fn is_non_zero(self: Player) -> bool { + !self.is_zero() + } +} + diff --git a/contracts/src/store.cairo b/contracts/src/store.cairo new file mode 100644 index 0000000..3c05b1b --- /dev/null +++ b/contracts/src/store.cairo @@ -0,0 +1,57 @@ +//! Store struct and component management methods. + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Models imports + +use rpg::models::player::Player; +use rpg::models::dungeon::Dungeon; + +// Structs + +#[derive(Copy, Drop)] +struct Store { + world: IWorldDispatcher, +} + +// Implementations + +#[generate_trait] +impl StoreImpl of StoreTrait { + #[inline] + fn new(world: IWorldDispatcher) -> Store { + Store { world: world } + } + + #[inline] + fn get_state(self: Store, player_id: felt252) -> (Player, Dungeon) { + get!(self.world, player_id, (Player, Dungeon)) + } + + #[inline] + fn get_player(self: Store, player_id: felt252) -> Player { + get!(self.world, player_id, (Player)) + } + + #[inline] + fn get_dungeon(self: Store, player_id: felt252) -> Dungeon { + get!(self.world, player_id, (Dungeon)) + } + + #[inline] + fn set_state(self: Store, player: Player, dungeon: Dungeon) { + set!(self.world, (player, dungeon)) + } + + #[inline] + fn set_player(self: Store, player: Player) { + set!(self.world, (player)) + } + + #[inline] + fn set_dungeon(self: Store, dungeon: Dungeon) { + set!(self.world, (dungeon)) + } +} diff --git a/contracts/src/systems/actions.cairo b/contracts/src/systems/actions.cairo new file mode 100644 index 0000000..ecc7b28 --- /dev/null +++ b/contracts/src/systems/actions.cairo @@ -0,0 +1,77 @@ +// Starknet imports + +use starknet::ContractAddress; + +// Dojo imports + +use dojo::world::IWorldDispatcher; + +// Interfaces + +#[starknet::interface] +trait IActions { + fn spawn(self: @TContractState, name: felt252, role: u8); + fn move(self: @TContractState, direction: u8); + fn attack(self: @TContractState); + fn heal(self: @TContractState, quantity: u8); +} + +// Contracts + +#[dojo::contract] +mod actions { + // Component imports + + use rpg::components::playable::PlayableComponent; + + // Internal imports + + use rpg::types::mode::Mode; + + // Local imports + + use super::IActions; + + // Components + + component!(path: PlayableComponent, storage: playable, event: PlayableEvent); + impl PlayableInternalImpl = PlayableComponent::InternalImpl; + + // Storage + + #[storage] + struct Storage { + #[substorage(v0)] + playable: PlayableComponent::Storage, + } + + // Events + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + PlayableEvent: PlayableComponent::Event, + } + + // Implementations + + #[abi(embed_v0)] + impl ActionsImpl of IActions { + fn spawn(self: @ContractState, name: felt252, role: u8) { + self.playable.spawn(self.world(), name, role, Mode::Medium.into()) + } + + fn move(self: @ContractState, direction: u8) { + self.playable.move(self.world(), direction) + } + + fn attack(self: @ContractState) { + self.playable.attack(self.world()) + } + + fn heal(self: @ContractState, quantity: u8) { + self.playable.heal(self.world(), quantity) + } + } +} diff --git a/contracts/src/tests/setup.cairo b/contracts/src/tests/setup.cairo new file mode 100644 index 0000000..17af8c2 --- /dev/null +++ b/contracts/src/tests/setup.cairo @@ -0,0 +1,67 @@ +mod setup { + // Core imports + + use core::debug::PrintTrait; + + // Starknet imports + + use starknet::ContractAddress; + use starknet::testing::{set_contract_address, set_caller_address}; + + // Dojo imports + + use dojo::world::{IWorldDispatcherTrait, IWorldDispatcher}; + use dojo::utils::test::{spawn_test_world}; + + // Internal imports + + use rpg::models::index; + use rpg::models::player::Player; + use rpg::models::dungeon::Dungeon; + use rpg::types::role::Role; + use rpg::types::mode::Mode; + use rpg::systems::actions::{actions, IActions, IActionsDispatcher, IActionsDispatcherTrait}; + + // Constants + + fn PLAYER() -> ContractAddress { + starknet::contract_address_const::<'PLAYER'>() + } + + const PLAYER_NAME: felt252 = 'PLAYER'; + + #[derive(Drop)] + struct Systems { + actions: IActionsDispatcher, + } + + #[derive(Drop)] + struct Context { + player_id: felt252, + player_name: felt252, + } + + #[inline(always)] + fn spawn_game() -> (IWorldDispatcher, Systems, Context) { + // [Setup] World + let models = array![index::player::TEST_CLASS_HASH, index::dungeon::TEST_CLASS_HASH,]; + let world = spawn_test_world("dojo_starter_rpg", models); + + // [Setup] Systems + let actions_address = world + .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); + let systems = Systems { + actions: IActionsDispatcher { contract_address: actions_address }, + }; + world.grant_writer(dojo::utils::bytearray_hash(@"dojo_starter_rpg"), actions_address); + world.grant_writer(dojo::utils::bytearray_hash(@"dojo_starter_rpg"), PLAYER()); + + // [Setup] Context + set_contract_address(PLAYER()); + systems.actions.spawn(PLAYER_NAME, Role::Water.into()); + let context = Context { player_id: PLAYER().into(), player_name: PLAYER_NAME, }; + + // [Return] + (world, systems, context) + } +} diff --git a/contracts/src/tests/test_attack.cairo b/contracts/src/tests/test_attack.cairo new file mode 100644 index 0000000..4bf6cbf --- /dev/null +++ b/contracts/src/tests/test_attack.cairo @@ -0,0 +1,45 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::{set_contract_address, set_transaction_hash}; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use rpg::store::{Store, StoreTrait}; +use rpg::models::player::{Player, PlayerTrait}; +use rpg::models::dungeon::{Dungeon, DungeonTrait}; +use rpg::types::direction::Direction; +use rpg::systems::actions::IActionsDispatcherTrait; +use rpg::tests::setup::{setup, setup::{Systems, PLAYER}}; + +#[test] +fn test_actions_attack() { + // [Setup] + let (world, systems, context) = setup::spawn_game(); + let store = StoreTrait::new(world); + + // [Move] + systems.actions.move(Direction::Up.into()); + + // [Attack] Till death + loop { + let dungeon = store.get_dungeon(context.player_id); + if dungeon.health == 0 { + break; + } + systems.actions.attack(); + }; + + // [Assert] + let (player, dungeon) = store.get_state(context.player_id); + assert(player.health > 0, 'Attack: player health'); + assert(player.gold > 0, 'Attack: player gold'); + assert(dungeon.health == 0, 'Attack: dungeon health'); +} diff --git a/contracts/src/tests/test_heal.cairo b/contracts/src/tests/test_heal.cairo new file mode 100644 index 0000000..7297816 --- /dev/null +++ b/contracts/src/tests/test_heal.cairo @@ -0,0 +1,56 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::{set_contract_address, set_transaction_hash}; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use rpg::store::{Store, StoreTrait}; +use rpg::models::player::{Player, PlayerTrait}; +use rpg::models::dungeon::{Dungeon, DungeonTrait}; +use rpg::types::direction::Direction; +use rpg::systems::actions::IActionsDispatcherTrait; +use rpg::tests::setup::{setup, setup::{Systems, PLAYER}}; + +#[test] +fn test_actions_heal() { + // [Setup] + let (world, systems, context) = setup::spawn_game(); + let store = StoreTrait::new(world); + + // [Move] + systems.actions.move(Direction::Up.into()); + + // [Attack] Till death + loop { + let dungeon = store.get_dungeon(context.player_id); + if dungeon.health == 0 { + break; + } + systems.actions.attack(); + }; + + // [Move] + systems.actions.move(Direction::Up.into()); + let mut dungeon = store.get_dungeon(context.player_id); + dungeon.monster = 0; + store.set_dungeon(dungeon); + + // [Heal] + let player = store.get_player(context.player_id); + let player_health = player.health; + let player_gold = player.gold; + systems.actions.heal(1); + + // [Assert] + let (player, _dungeon) = store.get_state(context.player_id); + assert(player.health > player_health, 'Attack: player health'); + assert(player.gold < player_gold, 'Attack: player gold'); +} diff --git a/contracts/src/tests/test_move.cairo b/contracts/src/tests/test_move.cairo new file mode 100644 index 0000000..162f194 --- /dev/null +++ b/contracts/src/tests/test_move.cairo @@ -0,0 +1,34 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::{set_contract_address, set_transaction_hash}; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use rpg::store::{Store, StoreTrait}; +use rpg::models::player::{Player, PlayerTrait}; +use rpg::models::dungeon::{Dungeon, DungeonTrait}; +use rpg::types::direction::Direction; +use rpg::systems::actions::IActionsDispatcherTrait; +use rpg::tests::setup::{setup, setup::{Systems, PLAYER}}; + +#[test] +fn test_actions_move() { + // [Setup] + let (world, systems, context) = setup::spawn_game(); + let store = StoreTrait::new(world); + + // [Move] + systems.actions.move(Direction::Up.into()); + + // [Assert] + let (_player, dungeon) = store.get_state(context.player_id); + assert(dungeon.health > 0, 'Move: dungeon health'); +} diff --git a/contracts/src/tests/test_setup.cairo b/contracts/src/tests/test_setup.cairo new file mode 100644 index 0000000..2baf6a2 --- /dev/null +++ b/contracts/src/tests/test_setup.cairo @@ -0,0 +1,34 @@ +// Core imports + +use core::debug::PrintTrait; + +// Starknet imports + +use starknet::testing::{set_contract_address, set_transaction_hash}; + +// Dojo imports + +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + +// Internal imports + +use rpg::store::{Store, StoreTrait}; +use rpg::models::player::{Player, PlayerTrait}; +use rpg::models::dungeon::{Dungeon, DungeonTrait}; +use rpg::systems::actions::IActionsDispatcherTrait; + +// Test imports + +use rpg::tests::setup::{setup, setup::{Systems, PLAYER}}; + +#[test] +fn test_actions_setup() { + // [Setup] + let (world, _, context) = setup::spawn_game(); + let store = StoreTrait::new(world); + + // [Assert] + let (player, dungeon) = store.get_state(context.player_id); + assert(player.id == context.player_id, 'Setup: player id'); + assert(dungeon.health == 0, 'Setup: dungeon health'); +} diff --git a/contracts/src/types/direction.cairo b/contracts/src/types/direction.cairo new file mode 100644 index 0000000..5643a10 --- /dev/null +++ b/contracts/src/types/direction.cairo @@ -0,0 +1,57 @@ +#[derive(Copy, Drop)] +enum Direction { + None, + Left, + Right, + Up, + Down, +} + +impl IntoDirectionFelt252 of core::Into { + #[inline] + fn into(self: Direction) -> felt252 { + match self { + Direction::None => 'NONE', + Direction::Left => 'LEFT', + Direction::Right => 'RIGHT', + Direction::Up => 'UP', + Direction::Down => 'DOWN', + } + } +} + +impl IntoDirectionU8 of core::Into { + #[inline] + fn into(self: Direction) -> u8 { + match self { + Direction::None => 0, + Direction::Left => 1, + Direction::Right => 2, + Direction::Up => 3, + Direction::Down => 4, + } + } +} + +impl IntoU8Direction of core::Into { + #[inline] + fn into(self: u8) -> Direction { + let card: felt252 = self.into(); + match card { + 0 => Direction::None, + 1 => Direction::Left, + 2 => Direction::Right, + 3 => Direction::Up, + 4 => Direction::Down, + _ => Direction::None, + } + } +} + +impl DirectionPrint of core::debug::PrintTrait { + #[inline] + fn print(self: Direction) { + let felt: felt252 = self.into(); + felt.print(); + } +} diff --git a/contracts/src/types/mode.cairo b/contracts/src/types/mode.cairo new file mode 100644 index 0000000..2fa6bfb --- /dev/null +++ b/contracts/src/types/mode.cairo @@ -0,0 +1,82 @@ +// Internal imports + +use rpg::elements::modes; +use rpg::types::monster::Monster; +use rpg::types::role::Role; + +#[derive(Copy, Drop)] +enum Mode { + None, + Easy, + Medium, + Hard, +} + +#[generate_trait] +impl ModeImpl of ModeTrait { + #[inline] + fn monster(self: Mode, seed: felt252) -> Monster { + match self { + Mode::None => Monster::None, + Mode::Easy => modes::easy::Easy::monster(seed), + Mode::Medium => modes::medium::Medium::monster(seed), + Mode::Hard => modes::hard::Hard::monster(seed), + } + } + + #[inline] + fn role(self: Mode, seed: felt252, player_role: Role) -> Role { + match self { + Mode::None => Role::None, + Mode::Easy => modes::easy::Easy::role(seed, player_role), + Mode::Medium => modes::medium::Medium::role(seed, player_role), + Mode::Hard => modes::hard::Hard::role(seed, player_role), + } + } +} + +impl IntoModeFelt252 of core::Into { + #[inline(always)] + fn into(self: Mode) -> felt252 { + match self { + Mode::None => 'NONE', + Mode::Easy => 'EASY', + Mode::Medium => 'MEDIUM', + Mode::Hard => 'HARD', + } + } +} + +impl IntoModeU8 of core::Into { + #[inline(always)] + fn into(self: Mode) -> u8 { + match self { + Mode::None => 0, + Mode::Easy => 1, + Mode::Medium => 2, + Mode::Hard => 3, + } + } +} + +impl IntoU8Mode of core::Into { + #[inline(always)] + fn into(self: u8) -> Mode { + let card: felt252 = self.into(); + match card { + 0 => Mode::None, + 1 => Mode::Easy, + 2 => Mode::Medium, + 3 => Mode::Hard, + _ => Mode::None, + } + } +} + +impl ModePrint of core::debug::PrintTrait { + #[inline(always)] + fn print(self: Mode) { + let felt: felt252 = self.into(); + felt.print(); + } +} diff --git a/contracts/src/types/monster.cairo b/contracts/src/types/monster.cairo new file mode 100644 index 0000000..f575f2e --- /dev/null +++ b/contracts/src/types/monster.cairo @@ -0,0 +1,90 @@ +// Internal imports + +use rpg::elements::monsters; + +#[derive(Copy, Drop)] +enum Monster { + None, + Common, + Elite, + Boss, +} + +#[generate_trait] +impl MonsterImpl of MonsterTrait { + #[inline] + fn damage(self: Monster) -> u8 { + match self { + Monster::None => 0, + Monster::Common => monsters::common::Common::damage(), + Monster::Elite => monsters::elite::Elite::damage(), + Monster::Boss => monsters::boss::Boss::damage(), + } + } + + #[inline] + fn health(self: Monster) -> u8 { + match self { + Monster::None => 0, + Monster::Common => monsters::common::Common::health(), + Monster::Elite => monsters::elite::Elite::health(), + Monster::Boss => monsters::boss::Boss::health(), + } + } + + #[inline] + fn reward(self: Monster) -> u16 { + match self { + Monster::None => 0, + Monster::Common => monsters::common::Common::reward(), + Monster::Elite => monsters::elite::Elite::reward(), + Monster::Boss => monsters::boss::Boss::reward(), + } + } +} + +impl IntoMonsterFelt252 of core::Into { + #[inline] + fn into(self: Monster) -> felt252 { + match self { + Monster::None => 'NONE', + Monster::Common => 'COMMON', + Monster::Elite => 'ELITE', + Monster::Boss => 'BOSS', + } + } +} + +impl IntoMonsterU8 of core::Into { + #[inline] + fn into(self: Monster) -> u8 { + match self { + Monster::None => 0, + Monster::Common => 1, + Monster::Elite => 2, + Monster::Boss => 3, + } + } +} + +impl IntoU8Monster of core::Into { + #[inline] + fn into(self: u8) -> Monster { + let card: felt252 = self.into(); + match card { + 0 => Monster::None, + 1 => Monster::Common, + 2 => Monster::Elite, + 3 => Monster::Boss, + _ => Monster::None, + } + } +} + +impl MonsterPrint of core::debug::PrintTrait { + #[inline] + fn print(self: Monster) { + let felt: felt252 = self.into(); + felt.print(); + } +} diff --git a/contracts/src/types/role.cairo b/contracts/src/types/role.cairo new file mode 100644 index 0000000..b17190f --- /dev/null +++ b/contracts/src/types/role.cairo @@ -0,0 +1,98 @@ +// Internal imports + +use rpg::elements::roles; + +#[derive(Copy, Drop)] +enum Role { + None, + Fire, + Water, + Earth, + Air, +} + +#[generate_trait] +impl RoleImpl of RoleTrait { + #[inline] + fn weakness(self: Role) -> Role { + match self { + Role::None => Role::None, + Role::Fire => roles::fire::Fire::weakness(self), + Role::Water => roles::water::Water::weakness(self), + Role::Earth => roles::earth::Earth::weakness(self), + Role::Air => roles::air::Air::weakness(self), + } + } + + #[inline] + fn strength(self: Role) -> Role { + match self { + Role::None => Role::None, + Role::Fire => roles::fire::Fire::strength(self), + Role::Water => roles::water::Water::strength(self), + Role::Earth => roles::earth::Earth::strength(self), + Role::Air => roles::air::Air::strength(self), + } + } + + #[inline] + fn received_damage(self: Role, role: Role, damage: u8) -> u8 { + let role_id: u8 = self.into(); + if role_id == self.weakness().into() { + damage * 2 + } else if role_id == self.strength().into() { + damage / 2 + } else { + damage + } + } +} + +impl IntoRoleFelt252 of core::Into { + #[inline(always)] + fn into(self: Role) -> felt252 { + match self { + Role::None => 'NONE', + Role::Fire => 'FIRE', + Role::Water => 'WATER', + Role::Earth => 'EARTH', + Role::Air => 'AIR', + } + } +} + +impl IntoRoleU8 of core::Into { + #[inline(always)] + fn into(self: Role) -> u8 { + match self { + Role::None => 0, + Role::Fire => 1, + Role::Water => 2, + Role::Earth => 3, + Role::Air => 4, + } + } +} + +impl IntoU8Role of core::Into { + #[inline(always)] + fn into(self: u8) -> Role { + let card: felt252 = self.into(); + match card { + 0 => Role::None, + 1 => Role::Fire, + 2 => Role::Water, + 3 => Role::Earth, + 4 => Role::Air, + _ => Role::None, + } + } +} + +impl RolePrint of core::debug::PrintTrait { + #[inline(always)] + fn print(self: Role) { + let felt: felt252 = self.into(); + felt.print(); + } +}