1use std::collections::VecDeque;
5
6pub type PlayerId = u16;
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Team {
9 Attack,
10 Defend,
11}
12impl Team {
13 fn index(self) -> usize {
14 match self {
15 Self::Attack => 0,
16 Self::Defend => 1,
17 }
18 }
19 fn other(self) -> Self {
20 match self {
21 Self::Attack => Self::Defend,
22 Self::Defend => Self::Attack,
23 }
24 }
25}
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum Weapon {
28 Pistol,
29 Rifle,
30 Sniper,
31}
32impl Weapon {
33 pub fn price(self) -> u32 {
34 match self {
35 Self::Pistol => 200,
36 Self::Rifle => 2700,
37 Self::Sniper => 4750,
38 }
39 }
40}
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum Phase {
43 Buy,
44 Live,
45 Planted,
46 PostRound,
47 Finished,
48}
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum WinReason {
51 Elimination,
52 TimeExpired,
53 Explosion,
54 Defused,
55}
56#[derive(Clone, Copy, Debug, PartialEq)]
57pub enum Bomb {
58 Carried(PlayerId),
59 Dropped([f32; 3]),
60 Planted { site: usize, position: [f32; 3] },
61}
62#[derive(Clone, Copy, Debug)]
63pub struct Site {
64 pub center: [f32; 3],
65 pub radius: f32,
66}
67#[derive(Clone, Copy, Debug)]
68pub struct PlayerSetup {
69 pub id: PlayerId,
70 pub team: Team,
71 pub spawn: [f32; 3],
72}
73#[derive(Clone, Debug)]
74pub struct Player {
75 pub id: PlayerId,
76 pub team: Team,
77 pub position: [f32; 3],
78 pub health: u16,
79 pub money: u32,
80 pub weapon: Weapon,
81 pub armor: u16,
82 pub defuse_kit: bool,
83 spawn: [f32; 3],
84}
85impl Player {
86 pub fn alive(&self) -> bool {
87 self.health > 0
88 }
89}
90#[derive(Clone, Debug)]
91pub struct Config {
92 pub buy_seconds: f32,
93 pub round_seconds: f32,
94 pub bomb_seconds: f32,
95 pub plant_seconds: f32,
96 pub defuse_seconds: f32,
97 pub kit_defuse_seconds: f32,
98 pub intermission_seconds: f32,
99 pub rounds_to_win: u16,
100 pub starting_money: u32,
101 pub sites: Vec<Site>,
102}
103impl Default for Config {
104 fn default() -> Self {
105 Self {
106 buy_seconds: 10.0,
107 round_seconds: 115.0,
108 bomb_seconds: 40.0,
109 plant_seconds: 3.0,
110 defuse_seconds: 10.0,
111 kit_defuse_seconds: 5.0,
112 intermission_seconds: 3.0,
113 rounds_to_win: 13,
114 starting_money: 800,
115 sites: vec![
116 Site {
117 center: [10.0, 0.0, 10.0],
118 radius: 4.0,
119 },
120 Site {
121 center: [-10.0, 0.0, 10.0],
122 radius: 4.0,
123 },
124 ],
125 }
126 }
127}
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum Action {
130 BuyWeapon(Weapon),
131 BuyArmor,
132 BuyDefuseKit,
133 Plant(usize),
134 Defuse,
135 Cancel,
136 DropBomb,
137 PickUpBomb,
138}
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum RuleError {
141 InvalidConfig,
142 InvalidRoster,
143 UnknownPlayer,
144 WrongPhase,
145 Dead,
146 WrongTeam,
147 InsufficientFunds,
148 AlreadyOwned,
149 BombUnavailable,
150 OutOfRange,
151 Busy,
152 InvalidInput,
153}
154#[derive(Clone, Copy, Debug, PartialEq, Eq)]
155pub enum Event {
156 RoundStarted(u32),
157 Killed {
158 attacker: PlayerId,
159 victim: PlayerId,
160 },
161 Planted {
162 player: PlayerId,
163 site: usize,
164 },
165 RoundWon {
166 team: Team,
167 reason: WinReason,
168 },
169 MatchWon(Team),
170}
171#[derive(Clone, Copy)]
172enum Interaction {
173 Plant(usize),
174 Defuse,
175}
176#[derive(Clone, Copy)]
177struct Channel {
178 player: PlayerId,
179 action: Interaction,
180 elapsed: f32,
181 required: f32,
182 origin: [f32; 3],
183}
184
185pub struct TacticalMatch {
186 config: Config,
187 players: Vec<Player>,
188 phase: Phase,
189 remaining: f32,
190 bomb: Bomb,
191 channel: Option<Channel>,
192 score: [u16; 2],
193 loss_streak: [u8; 2],
194 round: u32,
195 last_winner: Option<Team>,
196 events: VecDeque<Event>,
197}
198impl TacticalMatch {
199 pub fn new(config: Config, roster: Vec<PlayerSetup>) -> Result<Self, RuleError> {
200 if ![
201 config.buy_seconds,
202 config.round_seconds,
203 config.bomb_seconds,
204 config.plant_seconds,
205 config.defuse_seconds,
206 config.kit_defuse_seconds,
207 config.intermission_seconds,
208 ]
209 .into_iter()
210 .all(|n| n.is_finite() && n > 0.0 && n <= 3600.0)
211 || config.rounds_to_win == 0
212 || config.starting_money > 16000
213 || config.sites.is_empty()
214 || config.sites.len() > 32
215 || config
216 .sites
217 .iter()
218 .any(|site| !finite(site.center) || !site.radius.is_finite() || site.radius <= 0.0)
219 {
220 return Err(RuleError::InvalidConfig);
221 }
222 if roster.len() < 2
223 || roster.len() > 64
224 || !roster.iter().any(|p| p.team == Team::Attack)
225 || !roster.iter().any(|p| p.team == Team::Defend)
226 || roster
227 .iter()
228 .enumerate()
229 .any(|(i, p)| !finite(p.spawn) || roster[..i].iter().any(|old| old.id == p.id))
230 {
231 return Err(RuleError::InvalidRoster);
232 }
233 let carrier = roster
234 .iter()
235 .find(|p| p.team == Team::Attack)
236 .expect("validated attackers")
237 .id;
238 let players = roster
239 .into_iter()
240 .map(|p| Player {
241 id: p.id,
242 team: p.team,
243 position: p.spawn,
244 spawn: p.spawn,
245 health: 100,
246 money: config.starting_money,
247 weapon: Weapon::Pistol,
248 armor: 0,
249 defuse_kit: false,
250 })
251 .collect();
252 let remaining = config.buy_seconds;
253 let mut game = Self {
254 config,
255 players,
256 phase: Phase::Buy,
257 remaining,
258 bomb: Bomb::Carried(carrier),
259 channel: None,
260 score: [0; 2],
261 loss_streak: [0; 2],
262 round: 1,
263 last_winner: None,
264 events: VecDeque::with_capacity(128),
265 };
266 game.emit(Event::RoundStarted(1));
267 Ok(game)
268 }
269 pub fn phase(&self) -> Phase {
270 self.phase
271 }
272 pub fn remaining(&self) -> f32 {
273 self.remaining
274 }
275 pub fn round(&self) -> u32 {
276 self.round
277 }
278 pub fn score(&self) -> [u16; 2] {
279 self.score
280 }
281 pub fn bomb(&self) -> Bomb {
282 self.bomb
283 }
284 pub fn players(&self) -> &[Player] {
285 &self.players
286 }
287 pub fn player(&self, id: PlayerId) -> Option<&Player> {
288 self.players.iter().find(|p| p.id == id)
289 }
290 pub fn interaction_progress(&self) -> Option<(PlayerId, f32)> {
291 self.channel
292 .map(|c| (c.player, (c.elapsed / c.required).clamp(0.0, 1.0)))
293 }
294 pub fn pop_event(&mut self) -> Option<Event> {
295 self.events.pop_front()
296 }
297 pub fn set_position(&mut self, id: PlayerId, position: [f32; 3]) -> Result<(), RuleError> {
300 if !finite(position) {
301 return Err(RuleError::InvalidInput);
302 }
303 let index = self.index(id)?;
304 if !self.players[index].alive() {
305 return Err(RuleError::Dead);
306 }
307 self.players[index].position = position;
308 if self
309 .channel
310 .is_some_and(|c| c.player == id && distance(c.origin, position) > 0.15)
311 {
312 self.channel = None;
313 }
314 Ok(())
315 }
316 pub fn act(&mut self, id: PlayerId, action: Action) -> Result<(), RuleError> {
317 let index = self.index(id)?;
318 let player = &self.players[index];
319 if !player.alive() {
320 return Err(RuleError::Dead);
321 }
322 match action {
323 Action::BuyWeapon(weapon) => {
324 if self.phase != Phase::Buy {
325 return Err(RuleError::WrongPhase);
326 }
327 if player.weapon == weapon {
328 return Err(RuleError::AlreadyOwned);
329 }
330 self.spend(index, weapon.price())?;
331 self.players[index].weapon = weapon;
332 }
333 Action::BuyArmor => {
334 if self.phase != Phase::Buy {
335 return Err(RuleError::WrongPhase);
336 }
337 if player.armor == 100 {
338 return Err(RuleError::AlreadyOwned);
339 }
340 self.spend(index, 650)?;
341 self.players[index].armor = 100;
342 }
343 Action::BuyDefuseKit => {
344 if self.phase != Phase::Buy {
345 return Err(RuleError::WrongPhase);
346 }
347 if player.team != Team::Defend {
348 return Err(RuleError::WrongTeam);
349 }
350 if player.defuse_kit {
351 return Err(RuleError::AlreadyOwned);
352 }
353 self.spend(index, 400)?;
354 self.players[index].defuse_kit = true;
355 }
356 Action::Cancel => {
357 if self.channel.is_some_and(|c| c.player == id) {
358 self.channel = None;
359 }
360 }
361 Action::Plant(site_index) => {
362 if self.phase != Phase::Live {
363 return Err(RuleError::WrongPhase);
364 }
365 if player.team != Team::Attack {
366 return Err(RuleError::WrongTeam);
367 }
368 if self.bomb != Bomb::Carried(id) {
369 return Err(RuleError::BombUnavailable);
370 }
371 let site = self
372 .config
373 .sites
374 .get(site_index)
375 .ok_or(RuleError::OutOfRange)?;
376 if distance(site.center, player.position) > site.radius {
377 return Err(RuleError::OutOfRange);
378 }
379 if self.channel.is_some() {
380 return Err(RuleError::Busy);
381 }
382 self.channel = Some(Channel {
383 player: id,
384 action: Interaction::Plant(site_index),
385 elapsed: 0.0,
386 required: self.config.plant_seconds,
387 origin: player.position,
388 });
389 }
390 Action::Defuse => {
391 if self.phase != Phase::Planted {
392 return Err(RuleError::WrongPhase);
393 }
394 if player.team != Team::Defend {
395 return Err(RuleError::WrongTeam);
396 }
397 let Bomb::Planted { position, .. } = self.bomb else {
398 return Err(RuleError::BombUnavailable);
399 };
400 if distance(position, player.position) > 2.5 {
401 return Err(RuleError::OutOfRange);
402 }
403 if self.channel.is_some() {
404 return Err(RuleError::Busy);
405 }
406 self.channel = Some(Channel {
407 player: id,
408 action: Interaction::Defuse,
409 elapsed: 0.0,
410 required: if player.defuse_kit {
411 self.config.kit_defuse_seconds
412 } else {
413 self.config.defuse_seconds
414 },
415 origin: player.position,
416 });
417 }
418 Action::DropBomb => {
419 if self.phase != Phase::Live {
420 return Err(RuleError::WrongPhase);
421 }
422 if self.bomb != Bomb::Carried(id) {
423 return Err(RuleError::BombUnavailable);
424 }
425 self.bomb = Bomb::Dropped(player.position);
426 self.channel = None;
427 }
428 Action::PickUpBomb => {
429 if self.phase != Phase::Live {
430 return Err(RuleError::WrongPhase);
431 }
432 if player.team != Team::Attack {
433 return Err(RuleError::WrongTeam);
434 }
435 let Bomb::Dropped(position) = self.bomb else {
436 return Err(RuleError::BombUnavailable);
437 };
438 if distance(player.position, position) > 1.5 {
439 return Err(RuleError::OutOfRange);
440 }
441 self.bomb = Bomb::Carried(id);
442 }
443 }
444 Ok(())
445 }
446 pub fn damage(
448 &mut self,
449 attacker: PlayerId,
450 victim: PlayerId,
451 amount: u16,
452 ) -> Result<(), RuleError> {
453 if !matches!(self.phase, Phase::Live | Phase::Planted) {
454 return Err(RuleError::WrongPhase);
455 }
456 let a = self.index(attacker)?;
457 let v = self.index(victim)?;
458 if !self.players[a].alive() || !self.players[v].alive() {
459 return Err(RuleError::Dead);
460 }
461 if self.players[a].team == self.players[v].team {
462 return Err(RuleError::WrongTeam);
463 }
464 let absorbed = self.players[v].armor.min(amount / 2);
465 self.players[v].armor -= absorbed;
466 self.players[v].health = self.players[v].health.saturating_sub(amount - absorbed);
467 if !self.players[v].alive() {
468 self.players[a].money = (self.players[a].money + 300).min(16000);
469 if self.bomb == Bomb::Carried(victim) {
470 self.bomb = Bomb::Dropped(self.players[v].position);
471 }
472 if self.channel.is_some_and(|c| c.player == victim) {
473 self.channel = None;
474 }
475 self.emit(Event::Killed { attacker, victim });
476 self.check_elimination();
477 }
478 Ok(())
479 }
480 pub fn tick(&mut self, mut dt: f32) -> Result<(), RuleError> {
483 if !dt.is_finite() || !(0.0..=3600.0).contains(&dt) {
484 return Err(RuleError::InvalidInput);
485 }
486 while dt > 0.0 && self.phase != Phase::Finished {
487 let channel_left = self
488 .channel
489 .map_or(f32::INFINITY, |c| (c.required - c.elapsed).max(0.0));
490 let step = dt.min(self.remaining).min(channel_left);
491 self.remaining = (self.remaining - step).max(0.0);
492 if let Some(channel) = &mut self.channel {
493 channel.elapsed += step;
494 }
495 dt = (dt - step).max(0.0);
496 if self.phase == Phase::Planted && self.remaining <= 1e-6 {
497 self.finish_round(Team::Attack, WinReason::Explosion);
498 continue;
499 }
500 if self.channel.is_some_and(|c| c.elapsed + 1e-6 >= c.required) {
501 self.finish_channel();
502 continue;
503 }
504 if self.remaining <= 1e-6 {
505 match self.phase {
506 Phase::Buy => {
507 self.phase = Phase::Live;
508 self.remaining = self.config.round_seconds;
509 }
510 Phase::Live => self.finish_round(Team::Defend, WinReason::TimeExpired),
511 Phase::PostRound => self.next_round(),
512 Phase::Planted | Phase::Finished => {
513 unreachable!("handled before phase transitions")
514 }
515 }
516 }
517 }
518 Ok(())
519 }
520 fn index(&self, id: PlayerId) -> Result<usize, RuleError> {
521 self.players
522 .iter()
523 .position(|p| p.id == id)
524 .ok_or(RuleError::UnknownPlayer)
525 }
526 fn spend(&mut self, index: usize, price: u32) -> Result<(), RuleError> {
527 let player = &mut self.players[index];
528 if player.money < price {
529 return Err(RuleError::InsufficientFunds);
530 }
531 player.money -= price;
532 Ok(())
533 }
534 fn finish_channel(&mut self) {
535 let channel = self.channel.take().expect("completed channel");
536 match channel.action {
537 Interaction::Plant(site) => {
538 let index = self.index(channel.player).expect("roster stable");
539 self.bomb = Bomb::Planted {
540 site,
541 position: channel.origin,
542 };
543 self.phase = Phase::Planted;
544 self.remaining = self.config.bomb_seconds;
545 self.players[index].money = (self.players[index].money + 300).min(16000);
546 self.emit(Event::Planted {
547 player: channel.player,
548 site,
549 });
550 }
551 Interaction::Defuse => self.finish_round(Team::Defend, WinReason::Defused),
552 }
553 }
554 fn check_elimination(&mut self) {
555 let attacks = self
556 .players
557 .iter()
558 .any(|p| p.team == Team::Attack && p.alive());
559 let defends = self
560 .players
561 .iter()
562 .any(|p| p.team == Team::Defend && p.alive());
563 if !defends {
564 self.finish_round(Team::Attack, WinReason::Elimination);
565 } else if !attacks && self.phase != Phase::Planted {
566 self.finish_round(Team::Defend, WinReason::Elimination);
567 }
568 }
569 fn finish_round(&mut self, winner: Team, reason: WinReason) {
570 if !matches!(self.phase, Phase::Live | Phase::Planted) {
571 return;
572 }
573 self.phase = Phase::PostRound;
574 self.remaining = self.config.intermission_seconds;
575 self.channel = None;
576 self.last_winner = Some(winner);
577 self.score[winner.index()] += 1;
578 self.loss_streak[winner.index()] = 0;
579 let loser = winner.other();
580 self.loss_streak[loser.index()] = (self.loss_streak[loser.index()] + 1).min(5);
581 let loss_reward = 1400 + u32::from(self.loss_streak[loser.index()] - 1) * 500;
582 for player in &mut self.players {
583 player.money = (player.money
584 + if player.team == winner {
585 3250
586 } else {
587 loss_reward
588 })
589 .min(16000);
590 }
591 self.emit(Event::RoundWon {
592 team: winner,
593 reason,
594 });
595 }
596 fn next_round(&mut self) {
597 let winner = self.last_winner.expect("post round has winner");
598 if self.score[winner.index()] >= self.config.rounds_to_win {
599 self.phase = Phase::Finished;
600 self.remaining = 0.0;
601 self.emit(Event::MatchWon(winner));
602 return;
603 }
604 self.round += 1;
605 self.phase = Phase::Buy;
606 self.remaining = self.config.buy_seconds;
607 for player in &mut self.players {
608 if !player.alive() {
609 player.weapon = Weapon::Pistol;
610 player.armor = 0;
611 player.defuse_kit = false;
612 }
613 player.health = 100;
614 player.position = player.spawn;
615 }
616 self.bomb = Bomb::Carried(
617 self.players
618 .iter()
619 .find(|p| p.team == Team::Attack)
620 .expect("roster has attackers")
621 .id,
622 );
623 self.emit(Event::RoundStarted(self.round));
624 }
625 fn emit(&mut self, event: Event) {
626 if self.events.len() == 128 {
627 self.events.pop_front();
628 }
629 self.events.push_back(event);
630 }
631}
632fn finite(v: [f32; 3]) -> bool {
633 v.into_iter().all(f32::is_finite)
634}
635fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
636 ((f64::from(a[0]) - f64::from(b[0])).powi(2)
637 + (f64::from(a[1]) - f64::from(b[1])).powi(2)
638 + (f64::from(a[2]) - f64::from(b[2])).powi(2))
639 .sqrt() as f32
640}
641
642#[cfg(test)]
643mod tests {
644 use super::*;
645 fn game() -> TacticalMatch {
646 TacticalMatch::new(
647 Config {
648 buy_seconds: 1.0,
649 round_seconds: 10.0,
650 bomb_seconds: 8.0,
651 plant_seconds: 2.0,
652 defuse_seconds: 4.0,
653 kit_defuse_seconds: 2.0,
654 intermission_seconds: 1.0,
655 rounds_to_win: 2,
656 starting_money: 4000,
657 sites: vec![Site {
658 center: [0.0; 3],
659 radius: 3.0,
660 }],
661 },
662 vec![
663 PlayerSetup {
664 id: 1,
665 team: Team::Attack,
666 spawn: [0.0; 3],
667 },
668 PlayerSetup {
669 id: 2,
670 team: Team::Defend,
671 spawn: [1.0, 0.0, 0.0],
672 },
673 ],
674 )
675 .unwrap()
676 }
677 #[test]
678 fn purchases_validate_team_phase_money_and_ownership() {
679 let mut g = game();
680 g.act(1, Action::BuyWeapon(Weapon::Rifle)).unwrap();
681 assert_eq!(g.player(1).unwrap().money, 1300);
682 assert_eq!(
683 g.act(1, Action::BuyWeapon(Weapon::Sniper)),
684 Err(RuleError::InsufficientFunds)
685 );
686 assert_eq!(g.act(1, Action::BuyDefuseKit), Err(RuleError::WrongTeam));
687 g.act(2, Action::BuyDefuseKit).unwrap();
688 assert_eq!(g.act(2, Action::BuyDefuseKit), Err(RuleError::AlreadyOwned));
689 g.tick(1.0).unwrap();
690 assert_eq!(g.act(1, Action::BuyArmor), Err(RuleError::WrongPhase));
691 }
692 #[test]
693 fn plant_defuse_requires_uninterrupted_distance_checked_channels() {
694 let mut g = game();
695 g.act(2, Action::BuyDefuseKit).unwrap();
696 g.tick(1.0).unwrap();
697 g.act(1, Action::Plant(0)).unwrap();
698 g.tick(1.0).unwrap();
699 g.set_position(1, [0.2, 0.0, 0.0]).unwrap();
700 assert!(g.interaction_progress().is_none());
701 g.act(1, Action::Plant(0)).unwrap();
702 g.tick(2.0).unwrap();
703 assert_eq!(g.phase(), Phase::Planted);
704 g.act(2, Action::Defuse).unwrap();
705 g.tick(2.0).unwrap();
706 assert_eq!(g.phase(), Phase::PostRound);
707 assert_eq!(g.score(), [0, 1]);
708 assert_eq!(g.player(1).unwrap().money, 5700); }
710 #[test]
711 fn planted_bomb_survives_attacker_elimination_and_expires() {
712 let mut g = game();
713 g.tick(1.0).unwrap();
714 g.act(1, Action::Plant(0)).unwrap();
715 g.tick(2.0).unwrap();
716 g.damage(2, 1, 100).unwrap();
717 assert_eq!(g.phase(), Phase::Planted);
718 g.tick(8.0).unwrap();
719 assert_eq!(g.score(), [1, 0]);
720 }
721 #[test]
722 fn elimination_economy_respawn_and_match_end() {
723 let mut g = game();
724 g.tick(1.0).unwrap();
725 g.damage(1, 2, 100).unwrap();
726 assert_eq!(g.player(1).unwrap().money, 7550);
727 assert_eq!(g.player(2).unwrap().money, 5400);
728 g.tick(1.0).unwrap();
729 assert_eq!(g.round(), 2);
730 assert!(g.player(2).unwrap().alive());
731 g.tick(1.0).unwrap();
732 g.damage(1, 2, 100).unwrap();
733 g.tick(1.0).unwrap();
734 assert_eq!(g.phase(), Phase::Finished);
735 assert_eq!(g.score(), [2, 0]);
736 assert_eq!(g.player(2).unwrap().money, 7300); }
738 #[test]
739 fn time_partitioning_and_exact_defuse_deadline_are_defined() {
740 let mut a = game();
741 let mut b = game();
742 a.tick(12.0).unwrap();
743 for _ in 0..120 {
744 b.tick(0.1).unwrap();
745 }
746 assert_eq!(a.phase(), b.phase());
747 assert_eq!(a.score(), b.score());
748 assert_eq!(a.round(), b.round());
749 let mut g = game();
750 g.tick(1.0).unwrap();
751 g.act(1, Action::Plant(0)).unwrap();
752 g.tick(2.0).unwrap();
753 g.tick(4.0).unwrap();
754 g.act(2, Action::Defuse).unwrap();
755 g.tick(4.0).unwrap();
756 assert_eq!(g.score(), [1, 0]); }
758 #[test]
759 fn dropped_bomb_rejects_defender_and_remote_pickup() {
760 let mut g = game();
761 g.tick(1.0).unwrap();
762 g.act(1, Action::DropBomb).unwrap();
763 assert_eq!(g.act(2, Action::PickUpBomb), Err(RuleError::WrongTeam));
764 g.set_position(1, [5.0, 0.0, 0.0]).unwrap();
765 assert_eq!(g.act(1, Action::PickUpBomb), Err(RuleError::OutOfRange));
766 g.set_position(1, [0.0; 3]).unwrap();
767 g.act(1, Action::PickUpBomb).unwrap();
768 assert_eq!(g.bomb(), Bomb::Carried(1));
769 }
770}