@crowdedkingdoms/ckx-kit 0.3.0-dev.1 → 0.4.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/crates/ckx-sdk/Cargo.toml +1 -1
- package/crates/ckx-sdk/src/kit/ai/brain.rs +288 -0
- package/crates/ckx-sdk/src/kit/ai/flow.rs +163 -0
- package/crates/ckx-sdk/src/kit/ai/mod.rs +10 -0
- package/crates/ckx-sdk/src/kit/ai/path.rs +214 -0
- package/crates/ckx-sdk/src/kit/ai/steering.rs +137 -0
- package/crates/ckx-sdk/src/kit/ai/turn.rs +141 -0
- package/crates/ckx-sdk/src/kit/mod.rs +49 -1
- package/crates/ckx-sdk/src/kit/play/abilities.rs +370 -0
- package/crates/ckx-sdk/src/kit/play/cards.rs +220 -0
- package/crates/ckx-sdk/src/kit/play/combat.rs +292 -0
- package/crates/ckx-sdk/src/kit/play/mod.rs +15 -0
- package/crates/ckx-sdk/src/kit/play/movement.rs +196 -0
- package/crates/ckx-sdk/src/kit/play/score.rs +183 -0
- package/crates/ckx-sdk/src/kit/play/timing.rs +218 -0
- package/crates/ckx-sdk/src/kit/play/turns.rs +315 -0
- package/crates/ckx-sdk/src/kit/presence.rs +142 -0
- package/crates/ckx-sdk/src/kit/rng.rs +10 -0
- package/crates/ckx-sdk/src/kit/sim/daycycle.rs +71 -0
- package/crates/ckx-sdk/src/kit/sim/mod.rs +10 -0
- package/crates/ckx-sdk/src/kit/sim/nodes.rs +109 -0
- package/crates/ckx-sdk/src/kit/sim/waves.rs +105 -0
- package/crates/ckx-sdk/src/kit/sim/weather.rs +142 -0
- package/crates/ckx-sdk/src/kit/sim/zones.rs +218 -0
- package/dist/cli/src/new.js +9 -0
- package/kit.pin +4 -4
- package/package.json +1 -1
- package/starters/arena/Cargo.toml +14 -0
- package/starters/arena/src/lib.rs +849 -0
- package/starters/ckx.json +12 -1
- package/starters/deck/Cargo.toml +14 -0
- package/starters/deck/src/lib.rs +510 -0
- package/starters/director/Cargo.toml +14 -0
- package/starters/director/src/lib.rs +628 -0
- package/starters/grid-mod/Cargo.toml +1 -1
- package/starters/instance/Cargo.toml +14 -0
- package/starters/instance/src/lib.rs +656 -0
- package/starters/leaderboard/Cargo.toml +14 -0
- package/starters/leaderboard/src/lib.rs +395 -0
- package/starters/matchmaker/Cargo.toml +1 -1
- package/starters/minigame/Cargo.toml +14 -0
- package/starters/minigame/src/lib.rs +391 -0
- package/starters/npc-mobs/Cargo.toml +1 -1
- package/starters/racing/Cargo.toml +14 -0
- package/starters/racing/src/lib.rs +1151 -0
- package/starters/session/Cargo.toml +1 -1
- package/starters/tactics/Cargo.toml +14 -0
- package/starters/tactics/src/lib.rs +562 -0
- package/starters/territory/Cargo.toml +14 -0
- package/starters/territory/src/lib.rs +878 -0
- package/starters/world-tick/Cargo.toml +1 -1
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
//! A duel, keyed by its table: two players, a hidden throw each round that the hub keeps until
|
|
2
|
+
//! both are in (or the round's time runs out), and the first to win a majority of `best_of`
|
|
3
|
+
//! rounds wins. Rock, paper, scissors is the game; replace [`HANDS`] and [`beats`] with yours (a
|
|
4
|
+
//! bid, a guess, a sealed move). The round is `ckx_sdk::kit::play::turns::RevealRound`, so
|
|
5
|
+
//! nobody sees a throw before both are in.
|
|
6
|
+
//!
|
|
7
|
+
//! Endpoints (players): `join`, `throw { hand }`, `leave` (before it starts), `rematch` (once it
|
|
8
|
+
//! is over), `state` (anyone). It speaks the kit protocol (`ckx_sdk::kit`): the `duel` topic
|
|
9
|
+
//! carries the table as `snapshot` messages, which say who has thrown but never what until the
|
|
10
|
+
//! reveal; `snapshot { topic: "duel" }` answers it; changes may carry an `action_id`; refusals
|
|
11
|
+
//! carry `kit:<code>:` codes. A player who does not throw in time loses the round.
|
|
12
|
+
|
|
13
|
+
use ckx_sdk::kit::play::turns::RevealRound;
|
|
14
|
+
use ckx_sdk::kit::{self, Publisher, Receipts, codes};
|
|
15
|
+
use ckx_sdk::prelude::*;
|
|
16
|
+
|
|
17
|
+
pub const TOPIC: &str = "duel";
|
|
18
|
+
pub const HANDS: [&str; 3] = ["rock", "paper", "scissors"];
|
|
19
|
+
const ROUND_TIMER: &str = "round";
|
|
20
|
+
|
|
21
|
+
/// Whether hand `a` beats hand `b`.
|
|
22
|
+
pub fn beats(a: &str, b: &str) -> bool {
|
|
23
|
+
matches!((a, b), ("rock", "scissors") | ("paper", "rock") | ("scissors", "paper"))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
27
|
+
#[serde(default)]
|
|
28
|
+
pub struct Config {
|
|
29
|
+
/// Rounds in a match; the first to win a majority wins it.
|
|
30
|
+
pub best_of: u32,
|
|
31
|
+
/// How long a round waits for both throws.
|
|
32
|
+
pub round_ms: u64,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Default for Config {
|
|
36
|
+
fn default() -> Self {
|
|
37
|
+
Self { best_of: 3, round_ms: 20_000 }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
42
|
+
#[serde(rename_all = "snake_case")]
|
|
43
|
+
pub enum Phase {
|
|
44
|
+
#[default]
|
|
45
|
+
Waiting,
|
|
46
|
+
Playing,
|
|
47
|
+
Finished,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
51
|
+
pub struct Seat {
|
|
52
|
+
pub player: u64,
|
|
53
|
+
pub wins: u32,
|
|
54
|
+
/// Whether this round's throw is in (never which).
|
|
55
|
+
pub thrown: bool,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// A revealed round.
|
|
59
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
|
60
|
+
pub struct Reveal {
|
|
61
|
+
pub round: u32,
|
|
62
|
+
/// Each seat's throw, in seat order; `None` when it was not in on time.
|
|
63
|
+
pub hands: Vec<Option<String>>,
|
|
64
|
+
pub winner: Option<u64>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// The table as everyone sees it.
|
|
68
|
+
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
|
69
|
+
pub struct DuelView {
|
|
70
|
+
pub phase: Phase,
|
|
71
|
+
pub seats: Vec<Seat>,
|
|
72
|
+
pub best_of: u32,
|
|
73
|
+
pub round: u32,
|
|
74
|
+
/// When this round closes.
|
|
75
|
+
pub deadline: Option<u64>,
|
|
76
|
+
pub last: Option<Reveal>,
|
|
77
|
+
pub winner: Option<u64>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[derive(Deserialize)]
|
|
81
|
+
struct Throw {
|
|
82
|
+
hand: String,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Serialize, Deserialize, Default, Debug)]
|
|
86
|
+
pub struct Duel {
|
|
87
|
+
pub config: Config,
|
|
88
|
+
pub phase: Phase,
|
|
89
|
+
pub players: Vec<u64>,
|
|
90
|
+
pub wins: Vec<u32>,
|
|
91
|
+
pub round: u32,
|
|
92
|
+
pub pending: Option<RevealRound>,
|
|
93
|
+
pub last: Option<Reveal>,
|
|
94
|
+
pub winner: Option<u64>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Duel {
|
|
98
|
+
pub fn view(&self) -> DuelView {
|
|
99
|
+
let thrown = |p: u64| self.pending.as_ref().is_some_and(|r| r.submissions.iter().any(|(id, _)| *id == p.to_string()));
|
|
100
|
+
DuelView {
|
|
101
|
+
phase: self.phase,
|
|
102
|
+
seats: self.players.iter().zip(&self.wins).map(|(&player, &wins)| Seat { player, wins, thrown: thrown(player) }).collect(),
|
|
103
|
+
best_of: self.config.best_of,
|
|
104
|
+
round: self.round,
|
|
105
|
+
deadline: self.pending.as_ref().map(|r| r.deadline_ms),
|
|
106
|
+
last: self.last.clone(),
|
|
107
|
+
winner: self.winner,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fn to_win(&self) -> u32 {
|
|
112
|
+
self.config.best_of.max(1) / 2 + 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Seats `player`; the second to sit starts the first round. Returns whether it started.
|
|
116
|
+
pub fn join(&mut self, player: u64, now: u64) -> Result<bool> {
|
|
117
|
+
if self.players.contains(&player) {
|
|
118
|
+
return Ok(false);
|
|
119
|
+
}
|
|
120
|
+
if self.players.len() >= 2 {
|
|
121
|
+
return Err(kit::error(codes::FULL, "the table has two players"));
|
|
122
|
+
}
|
|
123
|
+
self.players.push(player);
|
|
124
|
+
self.wins.push(0);
|
|
125
|
+
if self.players.len() == 2 {
|
|
126
|
+
self.next_round(now);
|
|
127
|
+
return Ok(true);
|
|
128
|
+
}
|
|
129
|
+
Ok(false)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub fn leave(&mut self, player: u64) -> Result<()> {
|
|
133
|
+
if self.phase == Phase::Playing {
|
|
134
|
+
return Err(kit::error(codes::STARTED, "the duel is under way"));
|
|
135
|
+
}
|
|
136
|
+
if let Some(i) = self.players.iter().position(|&p| p == player) {
|
|
137
|
+
self.players.remove(i);
|
|
138
|
+
self.wins.remove(i);
|
|
139
|
+
}
|
|
140
|
+
if self.phase == Phase::Finished {
|
|
141
|
+
self.phase = Phase::Waiting;
|
|
142
|
+
self.wins = vec![0; self.players.len()];
|
|
143
|
+
self.round = 0;
|
|
144
|
+
self.last = None;
|
|
145
|
+
self.winner = None;
|
|
146
|
+
}
|
|
147
|
+
Ok(())
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn next_round(&mut self, now: u64) {
|
|
151
|
+
self.phase = Phase::Playing;
|
|
152
|
+
self.round += 1;
|
|
153
|
+
self.pending = Some(RevealRound::new(self.players.iter().map(u64::to_string).collect(), now, self.config.round_ms));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Takes `player`'s hidden throw. Returns whether it completed the round.
|
|
157
|
+
pub fn throw(&mut self, player: u64, hand: &str, now: u64) -> Result<bool> {
|
|
158
|
+
if self.phase != Phase::Playing {
|
|
159
|
+
return Err(kit::error(codes::CONFLICT, "no round is open"));
|
|
160
|
+
}
|
|
161
|
+
if !HANDS.contains(&hand) {
|
|
162
|
+
return Err(kit::error(codes::BAD_REQUEST, format!("a hand is one of {}", HANDS.join(", "))));
|
|
163
|
+
}
|
|
164
|
+
let round = self.pending.as_mut().ok_or_else(|| kit::error(codes::CONFLICT, "no round is open"))?;
|
|
165
|
+
round.submit(&player.to_string(), serde_json::Value::String(hand.into())).map_err(|why| match why {
|
|
166
|
+
"already submitted" => kit::error(codes::CONFLICT, "your throw is in"),
|
|
167
|
+
_ => kit::error(codes::NOT_MEMBER, "you are not at this table"),
|
|
168
|
+
})?;
|
|
169
|
+
if round.complete() {
|
|
170
|
+
self.resolve(now);
|
|
171
|
+
return Ok(true);
|
|
172
|
+
}
|
|
173
|
+
Ok(false)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Reveals the round when both throws are in or its time is up. Returns whether it did.
|
|
177
|
+
pub fn resolve(&mut self, now: u64) -> bool {
|
|
178
|
+
if !self.pending.as_ref().is_some_and(|r| r.ready(now)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
let submissions = self.pending.take().map(RevealRound::reveal).unwrap_or_default();
|
|
182
|
+
let hands: Vec<Option<String>> = self
|
|
183
|
+
.players
|
|
184
|
+
.iter()
|
|
185
|
+
.map(|p| submissions.iter().find(|(id, _)| *id == p.to_string()).and_then(|(_, v)| v.as_str().map(String::from)))
|
|
186
|
+
.collect();
|
|
187
|
+
let winner = match (hands.first().cloned().flatten(), hands.get(1).cloned().flatten()) {
|
|
188
|
+
(Some(a), Some(b)) if beats(&a, &b) => Some(0),
|
|
189
|
+
(Some(a), Some(b)) if beats(&b, &a) => Some(1),
|
|
190
|
+
(Some(_), None) => Some(0),
|
|
191
|
+
(None, Some(_)) => Some(1),
|
|
192
|
+
_ => None,
|
|
193
|
+
};
|
|
194
|
+
if let Some(i) = winner {
|
|
195
|
+
self.wins[i] += 1;
|
|
196
|
+
}
|
|
197
|
+
self.last = Some(Reveal { round: self.round, hands, winner: winner.map(|i| self.players[i]) });
|
|
198
|
+
if let Some(i) = winner.filter(|&i| self.wins[i] >= self.to_win()) {
|
|
199
|
+
self.phase = Phase::Finished;
|
|
200
|
+
self.winner = Some(self.players[i]);
|
|
201
|
+
} else {
|
|
202
|
+
self.next_round(now);
|
|
203
|
+
}
|
|
204
|
+
true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pub fn rematch(&mut self, player: u64, now: u64) -> Result<()> {
|
|
208
|
+
if self.phase != Phase::Finished {
|
|
209
|
+
return Err(kit::error(codes::CONFLICT, "the duel is not over"));
|
|
210
|
+
}
|
|
211
|
+
if !self.players.contains(&player) {
|
|
212
|
+
return Err(kit::error(codes::NOT_MEMBER, "you are not at this table"));
|
|
213
|
+
}
|
|
214
|
+
self.wins = vec![0; self.players.len()];
|
|
215
|
+
self.round = 0;
|
|
216
|
+
self.last = None;
|
|
217
|
+
self.winner = None;
|
|
218
|
+
self.next_round(now);
|
|
219
|
+
Ok(())
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// The hub: the duel, and what the kit protocol keeps beside it.
|
|
224
|
+
#[derive(Serialize, Deserialize, Default, Debug)]
|
|
225
|
+
pub struct DuelHub {
|
|
226
|
+
#[serde(flatten)]
|
|
227
|
+
pub duel: Duel,
|
|
228
|
+
#[serde(default)]
|
|
229
|
+
pub kit: Publisher,
|
|
230
|
+
#[serde(default)]
|
|
231
|
+
pub receipts: Receipts,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
impl DuelHub {
|
|
235
|
+
fn act(&mut self, ctx: &Ctx, call: &Call<'_>, now: u64) -> Result<Vec<u8>> {
|
|
236
|
+
let round_before = self.duel.round;
|
|
237
|
+
let out = match call.method {
|
|
238
|
+
"join" => encode(&self.duel.join(call.player()?, now)?),
|
|
239
|
+
"leave" => {
|
|
240
|
+
self.duel.leave(call.player()?)?;
|
|
241
|
+
encode(&true)
|
|
242
|
+
}
|
|
243
|
+
"throw" => {
|
|
244
|
+
let t: Throw = kit::args(call)?;
|
|
245
|
+
encode(&self.duel.throw(call.player()?, &t.hand, now)?)
|
|
246
|
+
}
|
|
247
|
+
"rematch" => {
|
|
248
|
+
self.duel.rematch(call.player()?, now)?;
|
|
249
|
+
encode(&true)
|
|
250
|
+
}
|
|
251
|
+
other => return Err(Error::unknown_method(other)),
|
|
252
|
+
};
|
|
253
|
+
self.after(ctx, round_before)?;
|
|
254
|
+
out
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// Arms the round timer for a new round, or stops it, and publishes.
|
|
258
|
+
fn after(&mut self, ctx: &Ctx, round_before: u32) -> Result<()> {
|
|
259
|
+
match &self.duel.pending {
|
|
260
|
+
Some(_) if self.duel.round != round_before => ctx.timer_after(ROUND_TIMER, self.duel.config.round_ms)?,
|
|
261
|
+
None => {
|
|
262
|
+
ctx.cancel_timer(ROUND_TIMER);
|
|
263
|
+
}
|
|
264
|
+
_ => {}
|
|
265
|
+
}
|
|
266
|
+
if self.duel.phase == Phase::Finished {
|
|
267
|
+
ctx.persist_now();
|
|
268
|
+
}
|
|
269
|
+
self.kit.snapshot(ctx, TOPIC, &self.duel.view())
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
impl Hub for DuelHub {
|
|
274
|
+
fn spawn(_ctx: &Ctx, seed: &[u8]) -> Result<Self> {
|
|
275
|
+
let mut hub = Self::default();
|
|
276
|
+
if !seed.is_empty() {
|
|
277
|
+
hub.duel.config = serde_json::from_slice(seed).map_err(|e| Error::new(format!("seed is not a JSON config: {e}")))?;
|
|
278
|
+
}
|
|
279
|
+
Ok(hub)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn load(_ctx: &Ctx, snapshot: &[u8], _from_version: u64) -> Result<Self> {
|
|
283
|
+
decode(snapshot)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn persist(&mut self, _ctx: &Ctx) -> Result<Vec<u8>> {
|
|
287
|
+
encode(self)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn handle(&mut self, ctx: &Ctx, call: Call<'_>) -> Result<Vec<u8>> {
|
|
291
|
+
match call.method {
|
|
292
|
+
"state" => encode(&self.duel.view()),
|
|
293
|
+
kit::SNAPSHOT_METHOD => self.kit.snapshot_reply(ctx, TOPIC, &self.duel.view()),
|
|
294
|
+
_ => Receipts::run(self, |h| &mut h.receipts, &call, ctx.now_ms(), |h| h.act(ctx, &call, ctx.now_ms())),
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fn on_timer(&mut self, ctx: &Ctx, timer: &str) -> Result<()> {
|
|
299
|
+
if timer != ROUND_TIMER {
|
|
300
|
+
return Ok(());
|
|
301
|
+
}
|
|
302
|
+
let round_before = self.duel.round;
|
|
303
|
+
if self.duel.resolve(ctx.now_ms()) {
|
|
304
|
+
self.after(ctx, round_before)?;
|
|
305
|
+
}
|
|
306
|
+
Ok(())
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
ckx_sdk::export_hub!(DuelHub);
|
|
311
|
+
|
|
312
|
+
#[cfg(test)]
|
|
313
|
+
mod tests {
|
|
314
|
+
use super::*;
|
|
315
|
+
use ckx_sdk::kit::{Message, error_code};
|
|
316
|
+
use ckx_sdk::testing::TestHub;
|
|
317
|
+
use serde_json::json;
|
|
318
|
+
|
|
319
|
+
fn code(e: &CallError) -> Option<&str> {
|
|
320
|
+
error_code(&e.message).map(|c| c.0)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[test]
|
|
324
|
+
fn the_rules_go_round() {
|
|
325
|
+
assert!(beats("rock", "scissors") && beats("scissors", "paper") && beats("paper", "rock"));
|
|
326
|
+
assert!(!beats("rock", "rock") && !beats("rock", "paper"));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn throws_stay_hidden_until_both_are_in_and_a_majority_wins() {
|
|
331
|
+
let mut hub = TestHub::<DuelHub>::spawn("minigame", "table-1").unwrap();
|
|
332
|
+
assert!(!hub.call_typed::<_, bool>(Caller::Player(1), "join", &()).unwrap());
|
|
333
|
+
assert!(hub.call_typed::<_, bool>(Caller::Player(2), "join", &()).unwrap());
|
|
334
|
+
let e = hub.call(Caller::Player(3), "join", &encode(&()).unwrap()).unwrap_err();
|
|
335
|
+
assert_eq!(code(&e), Some(codes::FULL));
|
|
336
|
+
|
|
337
|
+
assert!(!hub.call_typed::<_, bool>(Caller::Player(1), "throw", &json!({ "hand": "rock" })).unwrap());
|
|
338
|
+
let view: DuelView = hub.call_typed(Caller::Player(2), "state", &()).unwrap();
|
|
339
|
+
assert!(view.seats[0].thrown && !view.seats[1].thrown);
|
|
340
|
+
let published = hub.published();
|
|
341
|
+
let (_, last) = published.last().unwrap();
|
|
342
|
+
assert!(!String::from_utf8_lossy(last).contains("rock"), "a throw is never published before the reveal");
|
|
343
|
+
let e = hub.call(Caller::Player(1), "throw", &encode(&json!({ "hand": "paper" })).unwrap()).unwrap_err();
|
|
344
|
+
assert_eq!(code(&e), Some(codes::CONFLICT));
|
|
345
|
+
let e = hub.call(Caller::Player(2), "throw", &encode(&json!({ "hand": "lizard" })).unwrap()).unwrap_err();
|
|
346
|
+
assert_eq!(code(&e), Some(codes::BAD_REQUEST));
|
|
347
|
+
|
|
348
|
+
assert!(hub.call_typed::<_, bool>(Caller::Player(2), "throw", &json!({ "hand": "scissors" })).unwrap());
|
|
349
|
+
let view: DuelView = hub.call_typed(Caller::Player(2), "state", &()).unwrap();
|
|
350
|
+
assert_eq!(view.last, Some(Reveal { round: 1, hands: vec![Some("rock".into()), Some("scissors".into())], winner: Some(1) }));
|
|
351
|
+
assert_eq!((view.round, view.seats[0].wins), (2, 1));
|
|
352
|
+
|
|
353
|
+
let _: bool = hub.call_typed(Caller::Player(1), "throw", &json!({ "hand": "paper" })).unwrap();
|
|
354
|
+
let _: bool = hub.call_typed(Caller::Player(2), "throw", &json!({ "hand": "rock" })).unwrap();
|
|
355
|
+
let view: DuelView = hub.call_typed(Caller::Player(2), "state", &()).unwrap();
|
|
356
|
+
assert_eq!((view.phase, view.winner), (Phase::Finished, Some(1)));
|
|
357
|
+
let snap: Message<DuelView> =
|
|
358
|
+
decode(&hub.call(Caller::Player(2), kit::SNAPSHOT_METHOD, &encode(&json!({ "topic": TOPIC })).unwrap()).unwrap()).unwrap();
|
|
359
|
+
assert_eq!(snap.body.winner, Some(1));
|
|
360
|
+
|
|
361
|
+
hub.call_typed::<_, bool>(Caller::Player(2), "rematch", &()).unwrap();
|
|
362
|
+
let view: DuelView = hub.call_typed(Caller::Player(2), "state", &()).unwrap();
|
|
363
|
+
assert_eq!((view.phase, view.round, view.seats[0].wins), (Phase::Playing, 1, 0));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn a_throw_not_in_on_time_loses_the_round_and_a_round_with_none_is_replayed() {
|
|
368
|
+
let mut hub = TestHub::<DuelHub>::spawn_with("minigame", "table-2", br#"{"best_of":1,"round_ms":5000}"#).unwrap();
|
|
369
|
+
let _: bool = hub.call_typed(Caller::Player(1), "join", &()).unwrap();
|
|
370
|
+
let _: bool = hub.call_typed(Caller::Player(2), "join", &()).unwrap();
|
|
371
|
+
hub.advance(6_000).unwrap();
|
|
372
|
+
let view: DuelView = hub.call_typed(Caller::Player(1), "state", &()).unwrap();
|
|
373
|
+
assert_eq!((view.phase, view.round, view.last.as_ref().map(|r| r.winner)), (Phase::Playing, 2, Some(None)));
|
|
374
|
+
let _: bool = hub.call_typed(Caller::Player(2), "throw", &json!({ "hand": "paper" })).unwrap();
|
|
375
|
+
hub.advance(6_000).unwrap();
|
|
376
|
+
let view: DuelView = hub.call_typed(Caller::Player(1), "state", &()).unwrap();
|
|
377
|
+
assert_eq!((view.phase, view.winner), (Phase::Finished, Some(2)));
|
|
378
|
+
assert_eq!(view.last.unwrap().hands, vec![None, Some("paper".into())]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#[test]
|
|
382
|
+
fn a_resent_throw_is_answered_once() {
|
|
383
|
+
let mut hub = TestHub::<DuelHub>::spawn("minigame", "table-3").unwrap();
|
|
384
|
+
let _: bool = hub.call_typed(Caller::Player(1), "join", &()).unwrap();
|
|
385
|
+
let _: bool = hub.call_typed(Caller::Player(2), "join", &()).unwrap();
|
|
386
|
+
let args = json!({ "hand": "rock", "action_id": "t-1" });
|
|
387
|
+
let first: bool = hub.call_typed(Caller::Player(1), "throw", &args).unwrap();
|
|
388
|
+
let again: bool = hub.call_typed(Caller::Player(1), "throw", &args).unwrap();
|
|
389
|
+
assert_eq!(first, again);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "racing"
|
|
3
|
+
description = "Starter for two types: `race`, a course of gates with a countdown, laps, splits, places and a ghost of the winner; and `ball`, two teams, a ball the hub moves, possession and goals"
|
|
4
|
+
version = "0.1.0"
|
|
5
|
+
edition = "2024"
|
|
6
|
+
publish = false
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
|
|
11
|
+
[dependencies]
|
|
12
|
+
ckx-sdk = "0.11.0"
|
|
13
|
+
serde = { version = "1", features = ["derive"] }
|
|
14
|
+
serde_json = "1"
|