diff --git a/src/interfaces/ISnooknet.cairo b/src/interfaces/ISnooknet.cairo index 3257584..22ee2cf 100644 --- a/src/interfaces/ISnooknet.cairo +++ b/src/interfaces/ISnooknet.cairo @@ -1,5 +1,5 @@ use dojo_starter::model::game_model::{Game}; - +use dojo_starter::model::tournament_model::TournamentReward; use starknet::{ContractAddress}; @@ -7,10 +7,22 @@ use starknet::{ContractAddress}; #[starknet::interface] pub trait ISnooknet { // fn register(ref self: T, username: felt252, is_anonymous: bool); + fn create_player(ref self: T); fn create_match(ref self: T, opponent: ContractAddress, stake_amount: u256) -> u256; fn create_new_game_id(ref self: T) -> u256; fn end_match(ref self: T, game_id: u256, winner: ContractAddress); fn retrieve_game(ref self: T, game_id: u256) -> Game; + + fn create_tournament( + ref self: T, + name: felt252, + max_players: u8, + start_date: u64, + end_date: u64, + rewards: Array, + ) -> u256; + fn join_tournament(ref self: T, tournament_id: u256); + fn end_tournament(ref self: T, tournament_id: u256); // fn mint_nft(ref self: T, asset_type: AssetType, rarity: Rarity) -> u256; // fn lease_nft(ref self: T, asset_id: u256, leasee: ContractAddress); // fn submit_proposal(ref self: T, proposal_id: u256); diff --git a/src/lib.cairo b/src/lib.cairo index c8aea43..497243d 100644 --- a/src/lib.cairo +++ b/src/lib.cairo @@ -4,6 +4,8 @@ pub mod systems { pub mod model { pub mod game_model; + pub mod player_model; + pub mod tournament_model; } pub mod interfaces { diff --git a/src/model/player_model.cairo b/src/model/player_model.cairo new file mode 100644 index 0000000..ad34029 --- /dev/null +++ b/src/model/player_model.cairo @@ -0,0 +1,135 @@ +use starknet::ContractAddress; + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::model] +pub struct Player { + #[key] + pub contract_address: ContractAddress, + pub leaderboard_position: u64, + pub xp: u256, + pub elo_rating: u256, + pub games_won: u64, + pub games_lost: u64, + pub nft_coins_available: u256, + pub level: u32, +} + +pub trait PlayerTrait { + // Create and return a new player + fn new( + contract_address: ContractAddress, + leaderboard_position: u64, + xp: u256, + elo_rating: u256, + games_won: u64, + games_lost: u64, + nft_coins_available: u256, + level: u32, + ) -> Player; +} + +impl PlayerImpl of PlayerTrait { + fn new( + contract_address: ContractAddress, + leaderboard_position: u64, + xp: u256, + elo_rating: u256, + games_won: u64, + games_lost: u64, + nft_coins_available: u256, + level: u32, + ) -> Player { + Player { + contract_address, + leaderboard_position, + xp, + elo_rating, + games_won, + games_lost, + nft_coins_available, + level, + } + } +} + +#[cfg(test)] +mod tests { + use super::{PlayerImpl}; + use starknet::contract_address::contract_address_const; + + #[test] + #[available_gas(999999)] + fn test_player_creation() { + let contract_address = contract_address_const::<'player_1'>(); + let leaderboard_position = 1_u64; + let xp = 100_u256; + let elo_rating = 1500_u256; + let games_won = 5_u64; + let games_lost = 2_u64; + let nft_coins_available = 10_u256; + let level = 1_u32; + + let player = PlayerImpl::new( + contract_address, + leaderboard_position, + xp, + elo_rating, + games_won, + games_lost, + nft_coins_available, + level, + ); + + assert(player.contract_address == contract_address, 'Contract address mismatch'); + assert( + player.leaderboard_position == leaderboard_position, 'Leaderboard position mismatch', + ); + assert(player.xp == xp, 'XP mismatch'); + assert(player.elo_rating == elo_rating, 'Elo rating mismatch'); + assert(player.games_won == games_won, 'Games won mismatch'); + assert(player.games_lost == games_lost, 'Games lost mismatch'); + assert(player.nft_coins_available == nft_coins_available, 'NFT coins mismatch'); + assert(player.level == level, 'Level mismatch'); + } + + #[test] + #[available_gas(999999)] + fn test_player_with_zero_values() { + let contract_address = contract_address_const::<'player_1'>(); + let player = PlayerImpl::new( + contract_address, 0_u64, 0_u256, 0_u256, 0_u64, 0_u64, 0_u256, 0_u32, + ); + + assert(player.contract_address == contract_address, 'Contract address mismatch'); + assert(player.leaderboard_position == 0_u64, 'Incorrect leaderboard position'); + assert(player.xp == 0_u256, 'XP should be 0'); + assert(player.elo_rating == 0_u256, 'Elo rating should be 0'); + assert(player.games_won == 0_u64, 'Games won should be 0'); + assert(player.games_lost == 0_u64, 'Games lost should be 0'); + assert(player.nft_coins_available == 0_u256, 'NFT coins should be 0'); + assert(player.level == 0_u32, 'Level should be 0'); + } + + #[test] + #[available_gas(999999)] + fn test_player_with_max_values() { + let contract_address = contract_address_const::<'player_1'>(); + let max_u64 = 18446744073709551615_u64; + let max_u256 = 340282366920938463463374607431768211455_u256; + let max_u32 = 4294967295_u32; + + let player = PlayerImpl::new( + contract_address, max_u64, max_u256, max_u256, max_u64, max_u64, max_u256, max_u32, + ); + + assert(player.contract_address == contract_address, 'Contract address mismatch'); + assert(player.leaderboard_position == max_u64, 'Incorrect leaderboard position'); + assert(player.xp == max_u256, 'XP should be max'); + assert(player.elo_rating == max_u256, 'Elo rating should be max'); + assert(player.games_won == max_u64, 'Games won should be max'); + assert(player.games_lost == max_u64, 'Games lost should be max'); + assert(player.nft_coins_available == max_u256, 'NFT coins should be max'); + assert(player.level == max_u32, 'Level should be max'); + } +} + diff --git a/src/model/tournament_model.cairo b/src/model/tournament_model.cairo new file mode 100644 index 0000000..40773d3 --- /dev/null +++ b/src/model/tournament_model.cairo @@ -0,0 +1,151 @@ +use starknet::{ContractAddress}; + +#[derive(Serde, Copy, Drop, Introspect)] +#[dojo::model] +pub struct TournamentCounter { + #[key] + pub id: felt252, + pub current_val: u256, +} + +#[derive(Copy, Drop, Serde, Debug, Introspect, PartialEq)] +pub enum TournamentStatus { + Pending, + Active, + Ended, +} + +#[derive(Drop, Serde, Debug, Introspect)] +pub struct TournamentReward { + pub position: u8, + pub amount: u256, + pub token_type: felt252, +} + +#[derive(Drop, Serde, Debug)] +#[dojo::model] +pub struct Tournament { + #[key] + pub id: u256, + pub name: felt252, + pub organizer: ContractAddress, + pub max_players: u8, + pub current_players: u8, + pub start_date: u64, + pub end_date: u64, + pub status: TournamentStatus, + pub rewards: Array, +} + +pub trait TournamentTrait { + fn new( + id: u256, + name: felt252, + organizer: ContractAddress, + max_players: u8, + start_date: u64, + end_date: u64, + rewards: Array, + ) -> Tournament; +} + +impl TournamentImpl of TournamentTrait { + fn new( + id: u256, + name: felt252, + organizer: ContractAddress, + max_players: u8, + start_date: u64, + end_date: u64, + rewards: Array, + ) -> Tournament { + Tournament { + id, + name, + organizer, + max_players, + current_players: 0, + start_date, + end_date, + status: TournamentStatus::Pending, + rewards, + } + } +} + + +#[cfg(test)] +mod tests { + use super::{TournamentImpl, TournamentStatus, TournamentReward}; + use starknet::contract_address::contract_address_const; + + #[test] + #[available_gas(999999)] + fn test_tournament_creation() { + let id = 1_u256; + let name = 'Test Tournament'; + let organizer = contract_address_const::<'organizer'>(); + let max_players = 8_u8; + let start_date = 1000_u64; + let end_date = 2000_u64; + + let mut rewards = ArrayTrait::new(); + rewards.append(TournamentReward { position: 1_u8, amount: 1000_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 2_u8, amount: 500_u256, token_type: 'ETH' }); + + let tournament = TournamentImpl::new( + id, name, organizer, max_players, start_date, end_date, rewards, + ); + + assert(tournament.id == id, 'ID mismatch'); + assert(tournament.name == name, 'Name mismatch'); + assert(tournament.organizer == organizer, 'Organizer mismatch'); + assert(tournament.max_players == max_players, 'Max players mismatch'); + assert(tournament.current_players == 0, 'Current players should be empty'); + assert(tournament.start_date == start_date, 'Start date mismatch'); + assert(tournament.end_date == end_date, 'End date mismatch'); + assert(tournament.status == TournamentStatus::Pending, 'Status should be Pending'); + assert(tournament.rewards.len() == 2, 'Rewards length mismatch'); + } + + #[test] + #[available_gas(999999)] + fn test_tournament_with_empty_rewards() { + let id = 2_u256; + let name = 'Empty Rewards Tournament'; + let organizer = contract_address_const::<'organizer'>(); + let max_players = 4_u8; + let start_date = 1000_u64; + let end_date = 2000_u64; + let rewards = ArrayTrait::new(); + + let tournament = TournamentImpl::new( + id, name, organizer, max_players, start_date, end_date, rewards, + ); + + assert(tournament.id == id, 'ID mismatch'); + assert(tournament.rewards.len() == 0, 'Rewards should be empty'); + } + + #[test] + #[available_gas(999999)] + fn test_tournament_with_max_values() { + let id = 340282366920938463463374607431768211455_u256; // max u256 + let name = 'Max Values Tournament'; + let organizer = contract_address_const::<'organizer'>(); + let max_players = 255_u8; // max u8 + let start_date = 18446744073709551615_u64; // max u64 + let end_date = 18446744073709551615_u64; + let rewards = ArrayTrait::new(); + + let tournament = TournamentImpl::new( + id, name, organizer, max_players, start_date, end_date, rewards, + ); + + assert(tournament.id == id, 'ID mismatch'); + assert(tournament.max_players == max_players, 'Max players mismatch'); + assert(tournament.start_date == start_date, 'Start date mismatch'); + assert(tournament.end_date == end_date, 'End date mismatch'); + } +} + diff --git a/src/systems/Snooknet.cairo b/src/systems/Snooknet.cairo index e69d0ee..d32080c 100644 --- a/src/systems/Snooknet.cairo +++ b/src/systems/Snooknet.cairo @@ -1,16 +1,30 @@ +use starknet::{ContractAddress, get_caller_address, get_block_timestamp, contract_address_const}; + +use dojo::model::{ModelStorage}; +use dojo::event::EventStorage; + + use dojo_starter::interfaces::ISnooknet::ISnooknet; use dojo_starter::model::game_model::{Game, GameTrait, GameState, GameCounter}; +use dojo_starter::model::tournament_model::{ + Tournament as TournamentModel, TournamentTrait, TournamentStatus, TournamentReward, + TournamentCounter, +}; +use dojo_starter::model::player_model::{Player, PlayerTrait}; + // dojo decorator #[dojo::contract] pub mod Snooknet { - use super::{ISnooknet, Game, GameTrait, GameCounter, GameState}; - use starknet::{ - ContractAddress, get_caller_address, get_block_timestamp, contract_address_const, - }; - use dojo::model::{ModelStorage}; - use dojo::event::EventStorage; + use super::*; + #[derive(Copy, Drop, Serde)] + #[dojo::event] + pub struct PlayerCreated { + #[key] + pub player: ContractAddress, + pub timestamp: u64, + } #[derive(Copy, Drop, Serde)] #[dojo::event] @@ -36,9 +50,48 @@ pub mod Snooknet { pub winner: ContractAddress, } + #[dojo::event] + #[derive(Copy, Drop, Serde)] + pub struct TournamentCreated { + #[key] + tournament_id: u256, + name: felt252, + organizer: ContractAddress, + start_date: u64, + end_date: u64, + } + + #[dojo::event] + #[derive(Copy, Drop, Serde)] + pub struct TournamentJoined { + #[key] + tournament_id: u256, + player: ContractAddress, + } + + #[dojo::event] + #[derive(Copy, Drop, Serde)] + pub struct TournamentEnded { + #[key] + tournament_id: u256, + end_date: u64, + } + #[abi(embed_v0)] impl SnooknetImpl of ISnooknet { + fn create_player(ref self: ContractState) { + let mut world = self.world_default(); + + let caller: ContractAddress = get_caller_address(); + + let new_player: Player = PlayerTrait::new(caller, 0, 0, 0, 0, 0, 0, 1); + + world.write_model(@new_player); + + world.emit_event(@PlayerCreated { player: caller, timestamp: get_block_timestamp() }); + } + fn create_new_game_id(ref self: ContractState) -> u256 { let mut world = self.world_default(); let mut game_counter: GameCounter = world.read_model('v0'); @@ -47,6 +100,7 @@ pub mod Snooknet { world.write_model(@game_counter); new_val } + fn create_match( ref self: ContractState, opponent: ContractAddress, stake_amount: u256, ) -> u256 { @@ -92,6 +146,7 @@ pub mod Snooknet { world.emit_event(@Winner { game_id, winner }); world.emit_event(@GameEnded { game_id, timestamp }); } + fn retrieve_game(ref self: ContractState, game_id: u256) -> Game { // Get default world let mut world = self.world_default(); @@ -99,6 +154,84 @@ pub mod Snooknet { let game: Game = world.read_model(game_id); game } + + fn create_tournament( + ref self: ContractState, + name: felt252, + max_players: u8, + start_date: u64, + end_date: u64, + rewards: Array, + ) -> u256 { + let mut world = self.world_default(); + let tournament_id = self.create_new_tournament_id(); + + // Validate input parameters + assert(max_players > 1, 'Max players less than 2'); + assert(start_date < end_date, 'Invalid start date'); + + let caller = get_caller_address(); + + // Create new tournament + let mut new_tournament: TournamentModel = TournamentTrait::new( + tournament_id, name, caller, max_players, start_date, end_date, rewards, + ); + + world.write_model(@new_tournament); + + world + .emit_event( + @TournamentCreated { + tournament_id, name, organizer: caller, start_date, end_date, + }, + ); + + tournament_id + } + + fn join_tournament(ref self: ContractState, tournament_id: u256) { + // Get the caller's address + let caller = get_caller_address(); + + let mut world = self.world_default(); + let mut tournament: TournamentModel = world.read_model(tournament_id); + + // Check if tournament is open for joining + assert(tournament.status == TournamentStatus::Pending, 'Tournament not open'); + + // Check if tournament is full + assert(tournament.current_players < tournament.max_players, 'Tournament is full'); + + // Update tournament with new players array + tournament.current_players += 1; + + // Store updated tournament + world.write_model(@tournament); + + // Emit event + world.emit_event(@TournamentJoined { tournament_id, player: caller }); + } + + fn end_tournament(ref self: ContractState, tournament_id: u256) { + let mut world = self.world_default(); + let mut tournament: TournamentModel = world.read_model(tournament_id); + + // Check if tournament is in progress + assert( + tournament.status == TournamentStatus::Pending + || tournament.status == TournamentStatus::Active, + 'Tournament is not in progress', + ); + + // Update tournament status and timestamp + tournament.status = TournamentStatus::Ended; + tournament.end_date = get_block_timestamp(); + + // Store updated tournament + world.write_model(@tournament); + + world.emit_event(@TournamentEnded { tournament_id, end_date: tournament.end_date }); + } } #[generate_trait] @@ -108,6 +241,15 @@ pub mod Snooknet { fn world_default(self: @ContractState) -> dojo::world::WorldStorage { self.world(@"Snooknet") } + + fn create_new_tournament_id(ref self: ContractState) -> u256 { + let mut world = self.world_default(); + let mut tournament_counter: TournamentCounter = world.read_model('v0'); + let new_val = tournament_counter.current_val + 1; + tournament_counter.current_val = new_val; + world.write_model(@tournament_counter); + new_val + } } } diff --git a/src/tests/test_world.cairo b/src/tests/test_world.cairo index 196aab8..74db6f7 100644 --- a/src/tests/test_world.cairo +++ b/src/tests/test_world.cairo @@ -1,7 +1,10 @@ #[cfg(test)] mod tests { + use starknet::contract_address::contract_address_const; + use starknet::testing::set_contract_address; + use dojo_cairo_test::WorldStorageTestTrait; - use dojo::model::{ModelStorage, ModelStorageTest}; + use dojo::model::{ModelStorage}; use dojo::world::WorldStorageTrait; use dojo_cairo_test::{ spawn_test_world, NamespaceDef, TestResource, ContractDefTrait, ContractDef, @@ -9,9 +12,11 @@ mod tests { use dojo_starter::systems::Snooknet::{Snooknet}; use dojo_starter::interfaces::ISnooknet::{ISnooknetDispatcher, ISnooknetDispatcherTrait}; + use dojo_starter::model::tournament_model::{TournamentStatus, TournamentReward, Tournament}; - use dojo_starter::model::game_model::{Game, m_Game, GameState, GameCounter, m_GameCounter}; - use starknet::{testing, get_caller_address, contract_address_const}; + use dojo_starter::model::game_model::{m_Game, GameState, m_GameCounter}; + use dojo_starter::model::tournament_model::{m_Tournament, m_TournamentCounter}; + use dojo_starter::model::player_model::{m_Player}; fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { @@ -22,6 +27,13 @@ mod tests { TestResource::Event(Snooknet::e_GameCreated::TEST_CLASS_HASH), TestResource::Event(Snooknet::e_Winner::TEST_CLASS_HASH), TestResource::Event(Snooknet::e_GameEnded::TEST_CLASS_HASH), + TestResource::Model(m_TournamentCounter::TEST_CLASS_HASH), + TestResource::Model(m_Tournament::TEST_CLASS_HASH), + TestResource::Event(Snooknet::e_TournamentCreated::TEST_CLASS_HASH), + TestResource::Event(Snooknet::e_TournamentJoined::TEST_CLASS_HASH), + TestResource::Event(Snooknet::e_TournamentEnded::TEST_CLASS_HASH), + TestResource::Model(m_Player::TEST_CLASS_HASH), + TestResource::Event(Snooknet::e_PlayerCreated::TEST_CLASS_HASH), TestResource::Contract(Snooknet::TEST_CLASS_HASH), ] .span(), @@ -38,7 +50,6 @@ mod tests { .span() } - #[test] fn test_create_game() { let caller_1 = contract_address_const::<'aji'>(); @@ -51,7 +62,7 @@ mod tests { let (contract_address, _) = world.dns(@"Snooknet").unwrap(); let actions_system = ISnooknetDispatcher { contract_address }; - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game_id = actions_system.create_match(player_1, 400); assert(game_id == 1, 'Wrong game id'); @@ -72,11 +83,11 @@ mod tests { let (contract_address, _) = world.dns(@"Snooknet").unwrap(); let actions_system = ISnooknetDispatcher { contract_address }; - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game_id = actions_system.create_match(player_1, 400); - testing::set_contract_address(player); + set_contract_address(player); let game_id_1 = actions_system.create_match(opponent, 1000); assert(game_id_1 == 2, 'Wrong game id'); println!("game_id: {}", game_id); @@ -94,13 +105,13 @@ mod tests { let (contract_address, _) = world.dns(@"Snooknet").unwrap(); let actions_system = ISnooknetDispatcher { contract_address }; - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game_id = actions_system.create_match(player_1, 400); - testing::set_contract_address(caller_1); + set_contract_address(caller_1); actions_system.end_match(game_id, caller_1); - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game = actions_system.retrieve_game(game_id); assert(game.winner == caller_1, 'Winner not set correctly'); @@ -121,16 +132,120 @@ mod tests { let (contract_address, _) = world.dns(@"Snooknet").unwrap(); let actions_system = ISnooknetDispatcher { contract_address }; - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game_id = actions_system.create_match(player_1, 400); - testing::set_contract_address(player); + set_contract_address(player); actions_system.end_match(game_id, caller_1); - testing::set_contract_address(caller_1); + set_contract_address(caller_1); let game = actions_system.retrieve_game(game_id); assert(game.winner == caller_1, 'Winner not set correctly'); assert(game.state == GameState::Finished, 'Game not ended'); } + + #[test] + fn test_create_tournament() { + let ndef = namespace_def(); + let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + + let (contract_address, _) = world.dns(@"Snooknet").unwrap(); + let actions_system = ISnooknetDispatcher { contract_address }; + + let organizer = contract_address_const::<'organizer'>(); + set_contract_address(organizer); + + let name = 'Test Tournament'; + let max_players = 8_u8; + let start_date = 1000_u64; + let end_date = 2000_u64; + + let mut rewards = ArrayTrait::new(); + rewards.append(TournamentReward { position: 1_u8, amount: 1000_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 2_u8, amount: 500_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 3_u8, amount: 250_u256, token_type: 'ETH' }); + + let tournament_id = actions_system + .create_tournament(name, max_players, start_date, end_date, rewards); + + let tournament: Tournament = world.read_model(tournament_id); + + assert(tournament.name == name, 'Name mismatch'); + assert(tournament.max_players == max_players, 'Max players mismatch'); + assert(tournament.start_date == start_date, 'Start date mismatch'); + assert(tournament.end_date == end_date, 'End date mismatch'); + assert(tournament.status == TournamentStatus::Pending, 'Status should be Pending'); + assert(tournament.rewards.len() == 3, 'Rewards length mismatch'); + } + + #[test] + fn test_join_tournament() { + let ndef = namespace_def(); + let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + + let (contract_address, _) = world.dns(@"Snooknet").unwrap(); + let actions_system = ISnooknetDispatcher { contract_address }; + + let organizer = contract_address_const::<'organizer'>(); + set_contract_address(organizer); + + let name = 'Test Tournament'; + let max_players = 8_u8; + let start_date = 1000_u64; + let end_date = 2000_u64; + + let mut rewards = ArrayTrait::new(); + rewards.append(TournamentReward { position: 1_u8, amount: 1000_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 2_u8, amount: 500_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 3_u8, amount: 250_u256, token_type: 'ETH' }); + + let tournament_id = actions_system + .create_tournament(name, max_players, start_date, end_date, rewards); + + // Create a player + let player_address = contract_address_const::<'player'>(); + set_contract_address(player_address); + + actions_system.create_player(); + // Join tournament + actions_system.join_tournament(tournament_id); + + let tournament: Tournament = world.read_model(tournament_id); + assert(tournament.current_players == 1, 'Player should be added'); + } + + #[test] + fn test_end_tournament() { + let ndef = namespace_def(); + let mut world = spawn_test_world([ndef].span()); + world.sync_perms_and_inits(contract_defs()); + + let (contract_address, _) = world.dns(@"Snooknet").unwrap(); + let actions_system = ISnooknetDispatcher { contract_address }; + + let organizer = contract_address_const::<'organizer'>(); + set_contract_address(organizer); + + let name = 'Test Tournament'; + let max_players = 8_u8; + let start_date = 1000_u64; + let end_date = 2000_u64; + + let mut rewards = ArrayTrait::new(); + rewards.append(TournamentReward { position: 1_u8, amount: 1000_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 2_u8, amount: 500_u256, token_type: 'ETH' }); + rewards.append(TournamentReward { position: 3_u8, amount: 250_u256, token_type: 'ETH' }); + + let tournament_id = actions_system + .create_tournament(name, max_players, start_date, end_date, rewards); + + // End tournament + actions_system.end_tournament(tournament_id); + + let tournament: Tournament = world.read_model(tournament_id); + assert(tournament.status == TournamentStatus::Ended, 'Tournament should be ended'); + } }