1#![no_std]
8
9mod pool;
10
11use soroban_kit::{storage, soroban_tools};
12
13use pool::{Ball, Pocket, Pool};
14use soroban_sdk::{
15 contract, contracterror, contractimpl, contractmeta, contracttype, token, vec, Address, Env,
16 Vec,
17};
18
19contractmeta!(key="desc", val="A snooker game contract with pool physics validation, optional payments and rewards, on Soroban.");
20
21const MAX_BALLS: u32 = 5;
22
23#[contracttype]
24pub enum DataKey {
25 Admin,
26 Table(Address),
27 LedgerTime(Address),
28}
29
30#[storage(Temporary)]
31#[contracttype]
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub struct Table {
34 pub balls: Vec<Ball>,
35 pub pockets: Vec<Pocket>,
36}
37
38#[storage(Temporary)]
39#[contracttype]
40#[derive(Clone, Debug, Eq, PartialEq)]
41pub struct Session {
42 pub ledger_time: u64,
43}
44
45#[storage(Instance)]
46#[contracttype]
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct Admin {
49 pub admin: Address,
50 pub payment_token: Address,
51 pub payment_amount: i128,
52 pub reward_token: Address,
53 pub reward_amount: i128,
54}
55
56#[contracterror]
57#[derive(Copy, Clone, Debug)]
58#[repr(u32)]
59pub enum Error {
60 NoAdmin = 1,
61 AlreadyInitialized = 2,
62 InvalidPoolTable = 3,
63}
64
65fn rand(x: &mut u64) -> u16 {
67 *x ^= *x << 21;
68 *x ^= *x >> 35;
69 *x ^= *x << 4;
70 let mask: u64 = (1 << 14) - 1; let masked_value = *x & mask;
73 (masked_value as u16) % 5001 + 2500
74}
75
76#[contract]
77struct Snooker;
78
79pub trait SnookerTrait {
80 fn insertcoin(env: Env, player: Address) -> Result<Table, Error>;
81 fn play(env: Env, player: Address, cue_balls: Vec<Ball>) -> Result<u32, Error>;
82 fn initialize(
83 env: Env,
84 admin: Address,
85 payment_token: Address,
86 payment_amount: i128,
87 reward_token: Address,
88 reward_amount: i128,
89 ) -> Result<bool, Error>;
90 fn withdraw(env: Env, account: Address, amount: i128) -> Result<i128, Error>;
91}
92
93#[contractimpl]
94impl SnookerTrait for Snooker {
95 fn insertcoin(env: Env, player: Address) -> Result<Table, Error> {
96 if !storage::has::<DataKey, Admin>(&env, &DataKey::Admin) {
97 return Err(Error::NoAdmin);
98 }
99
100 player.require_auth();
101
102 let admin_data = storage::get::<DataKey, Admin>(&env, &DataKey::Admin).unwrap();
104
105 if admin_data.payment_amount > 0 {
106 let token = token::Client::new(&env, &admin_data.payment_token);
107 token.transfer(
108 &player,
109 &env.current_contract_address(),
110 &admin_data.payment_amount,
111 );
112 }
113
114 let ledger = env.ledger();
117 let mut seed = ledger.timestamp() + u64::from(ledger.sequence() + 1);
118 let mut balls: Vec<Ball> = vec![&env];
119 let mut pockets: Vec<Pocket> = vec![&env];
120 for _i in 0..MAX_BALLS {
121 balls.push_back(Ball(i128::from(rand(&mut seed)), 6000, 0, 0));
122 pockets.push_back(Pocket(i128::from(rand(&mut seed)), 2000));
123 }
124
125 storage::set::<DataKey, Session>(
130 &env,
131 &DataKey::LedgerTime(player.clone()),
132 &Session {
133 ledger_time: ledger.timestamp(),
134 },
135 );
136
137 let table_key = DataKey::Table(player.clone());
139 let table = Table { balls, pockets };
140 storage::set::<DataKey, Table>(&env, &table_key, &table);
141 Ok(table)
142 }
143
144 fn play(env: Env, player: Address, cue_balls: Vec<Ball>) -> Result<u32, Error> {
145 player.require_auth();
146
147 let stamp: u64 =
149 storage::get::<DataKey, Session>(&env, &DataKey::LedgerTime(player.clone()))
150 .unwrap()
151 .ledger_time;
152
153 let table_key = &DataKey::Table(player.clone());
155 let table: Table = storage::get::<DataKey, Table>(&env, &table_key).unwrap();
156
157 if stamp + 180 < env.ledger().timestamp()
159 || table.balls.len() < MAX_BALLS
160 || table.pockets.len() < MAX_BALLS
161 {
162 return Err(Error::InvalidPoolTable);
163 }
164
165 storage::remove::<DataKey, Table>(&env, &table_key);
166
167 let mut score = 0;
170 let mut streak = 0;
171 for i in 0..MAX_BALLS {
172 let cue_ball = cue_balls.get(i).unwrap();
173 let ball = table.balls.get(i).unwrap();
174 let pocket = table.pockets.get(i).unwrap();
175 let mut pool = Pool(cue_ball, ball, pocket);
176
177 if pool.is_potted(&env) {
180 streak += 1;
181 if score == 0 {
182 score += 12;
183 }
184 } else {
185 streak = 0;
186 }
187 score += streak * 9;
188 if i == MAX_BALLS - 1 {
189 break;
190 }
191 }
192
193 let admin_data = storage::get::<DataKey, Admin>(&env, &DataKey::Admin).unwrap();
194
195 if score == 147 {
197 if admin_data.reward_amount > 0 {
198 let reward_contract: Address = admin_data.reward_token;
199 let reward_token = token::Client::new(&env, &reward_contract);
200 reward_token.transfer(
201 &env.current_contract_address(),
202 &player,
203 &admin_data.reward_amount,
204 );
205 }
206 }
207
208 Ok(score)
209 }
210
211 fn initialize(
212 env: Env,
213 admin: Address,
214 payment_token: Address,
215 payment_amount: i128,
216 reward_token: Address,
217 reward_amount: i128,
218 ) -> Result<bool, Error> {
219 if storage::has::<DataKey, Admin>(&env, &DataKey::Admin) {
220 return Err(Error::AlreadyInitialized);
221 }
222
223 storage::set::<DataKey, Admin>(
224 &env,
225 &DataKey::Admin,
226 &Admin {
227 admin,
228 payment_token,
229 payment_amount,
230 reward_token,
231 reward_amount,
232 },
233 );
234 Ok(true)
235 }
236
237 fn withdraw(env: Env, account: Address, amount: i128) -> Result<i128, Error> {
238 if !storage::has::<DataKey, Admin>(&env, &DataKey::Admin) {
239 return Err(Error::NoAdmin);
240 }
241
242 let admin_data = storage::get::<DataKey, Admin>(&env, &DataKey::Admin).unwrap();
243
244 admin_data.admin.require_auth();
245
246 let reward_token = token::Client::new(&env, &admin_data.reward_token);
247 let balance = reward_token.balance(&env.current_contract_address());
248 if amount <= balance {
249 reward_token.transfer(&env.current_contract_address(), &account, &amount);
250 }
251 Ok(balance)
252 }
253}
254
255mod test;