1use crate::board::TileType;
2use crate::unit::LightsaberType;
3use serde::de::Error;
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, HashMap};
6use std::fmt::Display;
7use std::hash::Hash;
8use std::ops::RangeInclusive;
9
10#[derive(Debug, PartialEq)]
15pub struct GameConfig {
16 pub player_lives: u64,
18
19 pub max_strikes: u64,
22
23 pub max_rounds: u64,
25
26 pub max_fight_rounds: u64,
29
30 pub power_mode_threshold: u64,
32
33 pub power_mode_modifier: f64,
36
37 pub lightsaber_modifier_red: LightsaberModifier,
38 pub lightsaber_modifier_green: LightsaberModifier,
39 pub lightsaber_modifier_blue: LightsaberModifier,
40
41 pub unit_probabilities: Vec<UnitProbability>,
44
45 pub timeout_lobby: u64,
48 pub timeout_lightsaber_shop_phase: u64,
49 pub timeout_unit_shop_phase: u64,
50 pub timeout_placement_phase: u64,
51 pub timeout_fight_phase: u64,
52 pub live_loss_on_defeat: LiveLoss,
53 pub tile_modifier_force: TileModifier,
54 pub tile_modifier_medic_center: TileModifier,
55 pub tile_modifier_lava: TileModifier,
56}
57
58impl GameConfig {
59 fn from_json(json: json::GameConfigJson) -> Result<Self, GameConfigError> {
60 let mut lightsaber_modifiers = json
61 .lightSaberModifiers
62 .iter()
63 .map(|(k, v)| {
64 if !(v.base_value.is_finite() && v.base_value >= 0.0) {
65 return Err(GameConfigError::NegativeLightsaberModifierBaseValue);
66 }
67 if !(v.interval.0.is_finite() && v.interval.1.is_finite() && v.interval.0 >= 0.0) {
68 return Err(GameConfigError::NegativeLightsaberModifierInterval);
69 }
70 if !(v.interval.0 <= v.interval.1) {
71 return Err(GameConfigError::ReverseLightsaberModifierInterval);
72 }
73 Ok((
74 *k,
75 LightsaberModifier {
76 base_value: v.base_value,
77 interval: v.interval,
78 },
79 ))
80 })
81 .collect::<Result<HashMap<_, _>, _>>()?;
82
83 Ok(Self {
84 player_lives: if json.playerLives > 0 {
85 json.playerLives
86 } else {
87 return Err(GameConfigError::InsufficientPlayerLives);
88 },
89 max_strikes: json.maxStrikes,
90 max_rounds: if json.maxRounds > 0 {
91 json.maxRounds
92 } else {
93 return Err(GameConfigError::InsufficientMaxRounds);
94 },
95 max_fight_rounds: if json.maxFightRounds > 0 {
96 json.maxFightRounds
97 } else {
98 return Err(GameConfigError::InsufficientMaxFightRounds);
99 },
100 power_mode_threshold: if json.powerModeThreshold > 2 {
101 json.powerModeThreshold
102 } else {
103 return Err(GameConfigError::InsufficientPowerModeThreshold);
104 },
105 power_mode_modifier: {
106 let pm = json.powerModeModifier;
107 if !(pm.is_finite() && (1.0..=2.0).contains(&pm)) {
108 return Err(GameConfigError::PowerModeModifierOutOfRange);
109 }
110 pm
111 },
112 lightsaber_modifier_red: lightsaber_modifiers
113 .remove(&LightsaberType::Red)
114 .ok_or(GameConfigError::MissingRedLightsaberModifiers)?,
115 lightsaber_modifier_green: lightsaber_modifiers
116 .remove(&LightsaberType::Green)
117 .ok_or(GameConfigError::MissingGreenLightsaberModifiers)?,
118 lightsaber_modifier_blue: lightsaber_modifiers
119 .remove(&LightsaberType::Blue)
120 .ok_or(GameConfigError::MissingBlueLightsaberModifiers)?,
121 unit_probabilities: {
122 let probs = json
123 .unitProbabilities
124 .iter()
125 .enumerate()
126 .map(|(n, p)| {
127 if !(p.level1.is_finite() && p.level1 <= 1.0 && p.level1 >= 0.0) {
128 return Err(GameConfigError::BadUnitProbabilitiesLevel1(n));
129 }
130 if p.level2 > 1.0 || p.level2 < 0.0 {
131 return Err(GameConfigError::BadUnitProbabilitiesLevel2(n));
132 }
133 let level3 = 1.0 - p.level1 - p.level2;
134 if level3 < -0.001 {
136 return Err(GameConfigError::UnitProbabilitiesTooLarge(n));
137 }
138 Ok(UnitProbability {
139 level1: p.level1,
140 level2: p.level2,
141 level3: level3.max(0.0),
142 })
143 })
144 .collect::<Result<Vec<_>, _>>()?;
145
146 if probs.len() != 8 {
147 return Err(GameConfigError::InvalidUnitProbabilitiesLength);
148 }
149 probs
150 },
151 timeout_lobby: *json
152 .timeouts
153 .get(&PhaseType::Lobby)
154 .ok_or(GameConfigError::MissingLobbyTimeout)?,
155 timeout_lightsaber_shop_phase: *json
156 .timeouts
157 .get(&PhaseType::LightsaberShopPhase)
158 .ok_or(GameConfigError::MissingLightsaberShopTimeout)?,
159 timeout_unit_shop_phase: *json
160 .timeouts
161 .get(&PhaseType::UnitShopPhase)
162 .ok_or(GameConfigError::MissingUnitShopTimeout)?,
163 timeout_placement_phase: *json
164 .timeouts
165 .get(&PhaseType::PlacementPhase)
166 .ok_or(GameConfigError::MissingPlacementTimeout)?,
167 timeout_fight_phase: *json
168 .timeouts
169 .get(&PhaseType::FightPhase)
170 .ok_or(GameConfigError::MissingFightTimeout)?,
171 live_loss_on_defeat: {
172 if let Some((first, _)) = json.liveLossOnDefeat.first_key_value() {
173 if *first != 1 {
174 return Err(GameConfigError::NonOneFirstLiveLossOnDefeat);
175 }
176 } else {
177 return Err(GameConfigError::EmptyLiveLossOnDefeat);
178 }
179 LiveLoss {
180 map: json.liveLossOnDefeat.clone(),
181 }
182 },
183 tile_modifier_force: json
184 .fieldModifiers
185 .get(&TileType::Force)
186 .ok_or(GameConfigError::MissingFieldModifierForce)?
187 .clone(),
188 tile_modifier_medic_center: json
189 .fieldModifiers
190 .get(&TileType::MedicCenter)
191 .ok_or(GameConfigError::MissingFieldModifierMedicCenter)?
192 .clone(),
193 tile_modifier_lava: json
194 .fieldModifiers
195 .get(&TileType::Lava)
196 .ok_or(GameConfigError::MissingFieldModifierLava)?
197 .clone(),
198 })
199 }
200}
201#[derive(PartialEq)]
202pub enum GameConfigError {
203 NegativeLightsaberModifierBaseValue,
204 NegativeLightsaberModifierInterval,
205 ReverseLightsaberModifierInterval,
206 InsufficientPlayerLives,
207 InsufficientMaxRounds,
208 InsufficientMaxFightRounds,
209 InsufficientPowerModeThreshold,
210 PowerModeModifierOutOfRange,
211 MissingRedLightsaberModifiers,
212 MissingGreenLightsaberModifiers,
213 MissingBlueLightsaberModifiers,
214 BadUnitProbabilitiesLevel1(usize),
215 BadUnitProbabilitiesLevel2(usize),
216 UnitProbabilitiesTooLarge(usize),
217 InvalidUnitProbabilitiesLength,
218 MissingLobbyTimeout,
219 MissingLightsaberShopTimeout,
220 MissingUnitShopTimeout,
221 MissingPlacementTimeout,
222 MissingFightTimeout,
223 NonOneFirstLiveLossOnDefeat,
224 EmptyLiveLossOnDefeat,
225 MissingFieldModifierForce,
226 MissingFieldModifierMedicCenter,
227 MissingFieldModifierLava,
228}
229impl Display for GameConfigError {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 Self::NegativeLightsaberModifierBaseValue => {
233 write!(f, "baseValue of lightsaber modifier must not be negative")
234 }
235 Self::NegativeLightsaberModifierInterval => {
236 write!(f, "interval of lightsaber modifier must not be negative")
237 }
238 Self::ReverseLightsaberModifierInterval => write!(
239 f,
240 "interval of lightsaber modifier's start must not be bigger than the end"
241 ),
242 Self::InsufficientPlayerLives => write!(f, "playerLives must be at least 1"),
243 Self::InsufficientMaxRounds => write!(f, "maxRounds must be at least 1"),
244 Self::InsufficientMaxFightRounds => write!(f, "maxFightRounds must be at least 1"),
245 Self::InsufficientPowerModeThreshold => {
246 write!(f, "powerModeThreshold must be bigger than 2")
247 }
248 Self::PowerModeModifierOutOfRange => {
249 write!(f, "powerModeModifier must be within [1, 2]")
250 }
251 Self::MissingRedLightsaberModifiers => write!(f, "missing red lightsaber modifiers"),
252 Self::MissingGreenLightsaberModifiers => {
253 write!(f, "missing green lightsaber modifiers")
254 }
255 Self::MissingBlueLightsaberModifiers => write!(f, "missing blue lightsaber modifiers"),
256 Self::BadUnitProbabilitiesLevel1(n) => {
257 write!(f, "unitProbabilities[{n}].level1 must be within [0, 1]")
258 }
259 Self::BadUnitProbabilitiesLevel2(n) => {
260 write!(f, "unitProbabilities[{n}].level2 must be within [0, 1]")
261 }
262 Self::UnitProbabilitiesTooLarge(n) => write!(
263 f,
264 "unitProbabilities[{n}]: probabilities must add up to <= 1"
265 ),
266 Self::InvalidUnitProbabilitiesLength => {
267 write!(f, "unitProbabilities must have exactly 8 entries")
268 }
269 Self::MissingLobbyTimeout => write!(f, "missing LOBBY timeout"),
270 Self::MissingLightsaberShopTimeout => {
271 write!(f, "missing LIGHTSABER_SHOP_PHASE timeout")
272 }
273 Self::MissingUnitShopTimeout => write!(f, "missing UNIT_SHOP_PHASE timeout"),
274 Self::MissingPlacementTimeout => write!(f, "missing PLACEMENT_PHASE timeout"),
275 Self::MissingFightTimeout => write!(f, "missing FIGHT_PHASE timeout"),
276 Self::NonOneFirstLiveLossOnDefeat => {
277 write!(f, "the first key of liveLossOnDefeat must be 1")
278 }
279 Self::EmptyLiveLossOnDefeat => write!(f, "liveLossOnDefeat must not be empty"),
280 Self::MissingFieldModifierForce => write!(f, "Missing fieldModifiers entry FORCE"),
281 Self::MissingFieldModifierMedicCenter => {
282 write!(f, "Missing fieldModifiers entry MEDIC_CENTER")
283 }
284 Self::MissingFieldModifierLava => write!(f, "Missing fieldModifiers entry LAVA"),
285 }
286 }
287}
288impl std::fmt::Debug for GameConfigError {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 write!(f, "{self}")
291 }
292}
293impl std::error::Error for GameConfigError {}
294
295impl<'de> Deserialize<'de> for GameConfig {
296 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
297 where
298 D: serde::Deserializer<'de>,
299 {
300 let json = json::GameConfigJson::deserialize(deserializer)?;
301
302 Self::from_json(json).map_err(|e| Error::custom(e.to_string()))
303 }
304}
305
306impl Serialize for GameConfig {
307 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308 where
309 S: serde::Serializer,
310 {
311 let mut json_conf = json::GameConfigJson {
312 playerLives: self.player_lives,
313 maxStrikes: self.max_strikes,
314 maxRounds: self.max_rounds,
315 maxFightRounds: self.max_fight_rounds,
316 powerModeThreshold: self.power_mode_threshold,
317 powerModeModifier: self.power_mode_modifier,
318 lightSaberModifiers: HashMap::new(),
319 unitProbabilities: self
320 .unit_probabilities
321 .iter()
322 .map(|prob| json::UnitProbabilityJson {
323 level1: prob.level1,
324 level2: prob.level2,
325 })
326 .collect(),
327 timeouts: HashMap::new(),
328 liveLossOnDefeat: self.live_loss_on_defeat.map.clone(),
329 fieldModifiers: HashMap::new(),
330 };
331 json_conf
332 .lightSaberModifiers
333 .insert(LightsaberType::Red, self.lightsaber_modifier_red.clone());
334 json_conf.lightSaberModifiers.insert(
335 LightsaberType::Green,
336 self.lightsaber_modifier_green.clone(),
337 );
338 json_conf
339 .lightSaberModifiers
340 .insert(LightsaberType::Blue, self.lightsaber_modifier_blue.clone());
341 json_conf
342 .timeouts
343 .insert(PhaseType::Lobby, self.timeout_lobby);
344 json_conf.timeouts.insert(
345 PhaseType::LightsaberShopPhase,
346 self.timeout_lightsaber_shop_phase,
347 );
348 json_conf
349 .timeouts
350 .insert(PhaseType::UnitShopPhase, self.timeout_unit_shop_phase);
351 json_conf
352 .timeouts
353 .insert(PhaseType::PlacementPhase, self.timeout_placement_phase);
354 json_conf
355 .timeouts
356 .insert(PhaseType::FightPhase, self.timeout_fight_phase);
357 json_conf
358 .fieldModifiers
359 .insert(TileType::Force, self.tile_modifier_force.clone());
360 json_conf.fieldModifiers.insert(
361 TileType::MedicCenter,
362 self.tile_modifier_medic_center.clone(),
363 );
364 json_conf
365 .fieldModifiers
366 .insert(TileType::Lava, self.tile_modifier_lava.clone());
367 json_conf.serialize(serializer)
368 }
369}
370
371#[derive(Debug, PartialEq)]
374pub struct UnitProbability {
375 pub level1: f64,
376 pub level2: f64,
377 pub level3: f64,
378}
379
380#[derive(Debug, PartialEq)]
383pub struct LiveLoss {
384 map: BTreeMap<u64, u64>,
385}
386
387impl LiveLoss {
388 pub fn get_loss(&self, round: u64) -> u64 {
393 *self
394 .map
395 .range(..=round)
396 .next_back()
397 .expect("Round should not be 0")
398 .1
399 }
400
401 pub(super) fn new(map: BTreeMap<u64, u64>) -> Self {
402 Self { map }
403 }
404}
405
406#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
408pub struct LightsaberModifier {
409 #[serde(rename = "baseValue")]
410 pub base_value: f64,
411 pub interval: (f64, f64),
412}
413impl LightsaberModifier {
414 pub fn interval(&self) -> RangeInclusive<f64> {
417 self.interval.0..=self.interval.1
418 }
419}
420
421#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
423pub struct TileModifier {
424 pub health: i64,
425 pub armor: i64,
426 pub attack: i64,
427}
428
429#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Clone, Copy)]
430#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
431pub enum PhaseType {
432 Lobby,
433 LightsaberShopPhase,
434 UnitShopPhase,
435 PlacementPhase,
436 FightPhase,
437}
438
439#[allow(non_snake_case)]
440mod json {
441 use super::*;
442
443 #[derive(Deserialize, Serialize)]
444 pub struct GameConfigJson {
445 pub playerLives: u64,
446 pub maxStrikes: u64,
447 pub maxRounds: u64,
448 pub maxFightRounds: u64,
449 pub powerModeThreshold: u64,
450 pub powerModeModifier: f64,
451 pub lightSaberModifiers: HashMap<LightsaberType, LightsaberModifier>,
452 pub unitProbabilities: Vec<UnitProbabilityJson>,
453 pub timeouts: HashMap<PhaseType, u64>,
454 pub liveLossOnDefeat: BTreeMap<u64, u64>,
455 pub fieldModifiers: HashMap<TileType, TileModifier>,
456 }
457
458 #[derive(Deserialize, Serialize, Clone)]
459 pub struct UnitProbabilityJson {
460 pub level1: f64,
461 pub level2: f64,
462 }
463}
464
465#[cfg(test)]
466mod test {
467 use crate::{
468 board::TileType,
469 config::{
470 GameConfig,
471 game::{
472 GameConfigError, LightsaberModifier, PhaseType, TileModifier,
473 json::UnitProbabilityJson,
474 },
475 },
476 unit::LightsaberType,
477 };
478
479 use super::json::GameConfigJson;
480
481 macro_rules! assert_err_matches {
482 ($pat:pat, $mod_cfg:expr) => {{
483 let mut valid = valid();
484 ($mod_cfg)(&mut valid);
485 match GameConfig::from_json(valid) {
486 Err(e) => {
487 if matches!(e, $pat) {
488 eprintln!("correctly got error {e:?}");
489 } else {
490 panic!("expected {}, but got '{e}'", stringify!($pat));
491 }
492 }
493 Ok(_) => panic!("expected {}, but got Ok(_)", stringify!($pat)),
494 }
495 }};
496 }
497
498 #[test]
499 fn from_json() {
500 assert_eq!(GameConfig::from_json(valid()).err(), None);
501
502 assert_err_matches!(
503 GameConfigError::NegativeLightsaberModifierBaseValue,
504 |cfg: &mut GameConfigJson| {
505 cfg.lightSaberModifiers
506 .get_mut(&LightsaberType::Red)
507 .unwrap()
508 .base_value = -0.5;
509 }
510 );
511 assert_err_matches!(
512 GameConfigError::NegativeLightsaberModifierInterval,
513 |cfg: &mut GameConfigJson| {
514 cfg.lightSaberModifiers
515 .get_mut(&LightsaberType::Green)
516 .unwrap()
517 .interval
518 .0 = -0.5;
519 }
520 );
521 assert_err_matches!(
522 GameConfigError::ReverseLightsaberModifierInterval,
523 |cfg: &mut GameConfigJson| {
524 cfg.lightSaberModifiers
525 .get_mut(&LightsaberType::Green)
526 .unwrap()
527 .interval = (0.7, 0.3);
528 }
529 );
530 assert_err_matches!(
531 GameConfigError::InsufficientPlayerLives,
532 |cfg: &mut GameConfigJson| {
533 cfg.playerLives = 0;
534 }
535 );
536 assert_err_matches!(
537 GameConfigError::InsufficientMaxRounds,
538 |cfg: &mut GameConfigJson| {
539 cfg.maxRounds = 0;
540 }
541 );
542 assert_err_matches!(
543 GameConfigError::InsufficientMaxFightRounds,
544 |cfg: &mut GameConfigJson| {
545 cfg.maxFightRounds = 0;
546 }
547 );
548 assert_err_matches!(
549 GameConfigError::InsufficientPowerModeThreshold,
550 |cfg: &mut GameConfigJson| {
551 cfg.powerModeThreshold = 2;
552 }
553 );
554 assert_err_matches!(
555 GameConfigError::PowerModeModifierOutOfRange,
556 |cfg: &mut GameConfigJson| {
557 cfg.powerModeModifier = 0.9;
558 }
559 );
560 assert_err_matches!(
561 GameConfigError::PowerModeModifierOutOfRange,
562 |cfg: &mut GameConfigJson| {
563 cfg.powerModeModifier = 2.1;
564 }
565 );
566 assert_err_matches!(
567 GameConfigError::MissingRedLightsaberModifiers,
568 |cfg: &mut GameConfigJson| {
569 cfg.lightSaberModifiers.remove(&LightsaberType::Red);
570 }
571 );
572 assert_err_matches!(
573 GameConfigError::MissingGreenLightsaberModifiers,
574 |cfg: &mut GameConfigJson| {
575 cfg.lightSaberModifiers.remove(&LightsaberType::Green);
576 }
577 );
578 assert_err_matches!(
579 GameConfigError::MissingBlueLightsaberModifiers,
580 |cfg: &mut GameConfigJson| {
581 cfg.lightSaberModifiers.remove(&LightsaberType::Blue);
582 }
583 );
584 assert_err_matches!(
585 GameConfigError::BadUnitProbabilitiesLevel1(1),
586 |cfg: &mut GameConfigJson| {
587 cfg.unitProbabilities[1].level1 = 1.1;
588 }
589 );
590 assert_err_matches!(
591 GameConfigError::BadUnitProbabilitiesLevel2(2),
592 |cfg: &mut GameConfigJson| {
593 cfg.unitProbabilities[2].level2 = 1.1;
594 }
595 );
596 assert_err_matches!(
597 GameConfigError::UnitProbabilitiesTooLarge(3),
598 |cfg: &mut GameConfigJson| {
599 cfg.unitProbabilities[3].level1 = 0.6;
600 cfg.unitProbabilities[3].level2 = 0.6;
601 }
602 );
603 assert_err_matches!(
604 GameConfigError::InvalidUnitProbabilitiesLength,
605 |cfg: &mut GameConfigJson| {
606 cfg.unitProbabilities.pop();
607 }
608 );
609 assert_err_matches!(
610 GameConfigError::InvalidUnitProbabilitiesLength,
611 |cfg: &mut GameConfigJson| {
612 cfg.unitProbabilities
613 .push(cfg.unitProbabilities.last().unwrap().clone());
614 }
615 );
616 assert_err_matches!(
617 GameConfigError::MissingLobbyTimeout,
618 |cfg: &mut GameConfigJson| {
619 cfg.timeouts.remove(&PhaseType::Lobby);
620 }
621 );
622 assert_err_matches!(
623 GameConfigError::MissingLightsaberShopTimeout,
624 |cfg: &mut GameConfigJson| {
625 cfg.timeouts.remove(&PhaseType::LightsaberShopPhase);
626 }
627 );
628 assert_err_matches!(
629 GameConfigError::MissingUnitShopTimeout,
630 |cfg: &mut GameConfigJson| {
631 cfg.timeouts.remove(&PhaseType::UnitShopPhase);
632 }
633 );
634 assert_err_matches!(
635 GameConfigError::MissingPlacementTimeout,
636 |cfg: &mut GameConfigJson| {
637 cfg.timeouts.remove(&PhaseType::PlacementPhase);
638 }
639 );
640 assert_err_matches!(
641 GameConfigError::MissingFightTimeout,
642 |cfg: &mut GameConfigJson| {
643 cfg.timeouts.remove(&PhaseType::FightPhase);
644 }
645 );
646 assert_err_matches!(
647 GameConfigError::NonOneFirstLiveLossOnDefeat,
648 |cfg: &mut GameConfigJson| {
649 cfg.liveLossOnDefeat.remove(&1);
650 }
651 );
652 assert_err_matches!(
653 GameConfigError::EmptyLiveLossOnDefeat,
654 |cfg: &mut GameConfigJson| {
655 cfg.liveLossOnDefeat.clear();
656 }
657 );
658 assert_err_matches!(
659 GameConfigError::MissingFieldModifierForce,
660 |cfg: &mut GameConfigJson| {
661 cfg.fieldModifiers.remove(&TileType::Force);
662 }
663 );
664 assert_err_matches!(
665 GameConfigError::MissingFieldModifierMedicCenter,
666 |cfg: &mut GameConfigJson| {
667 cfg.fieldModifiers.remove(&TileType::MedicCenter);
668 }
669 );
670 assert_err_matches!(
671 GameConfigError::MissingFieldModifierLava,
672 |cfg: &mut GameConfigJson| {
673 cfg.fieldModifiers.remove(&TileType::Lava);
674 }
675 );
676 }
677
678 fn valid() -> GameConfigJson {
679 GameConfigJson {
680 playerLives: 5,
681 maxStrikes: 3,
682 maxRounds: 10,
683 maxFightRounds: 7,
684 powerModeThreshold: 4,
685 powerModeModifier: 1.3,
686 lightSaberModifiers: [
687 (
688 LightsaberType::Red,
689 LightsaberModifier {
690 base_value: 0.2,
691 interval: (0.1, 0.3),
692 },
693 ),
694 (
695 LightsaberType::Green,
696 LightsaberModifier {
697 base_value: 0.25,
698 interval: (0.15, 0.35),
699 },
700 ),
701 (
702 LightsaberType::Blue,
703 LightsaberModifier {
704 base_value: 0.18,
705 interval: (0.03, 0.12),
706 },
707 ),
708 ]
709 .into_iter()
710 .collect(),
711 unitProbabilities: vec![
712 UnitProbabilityJson {
713 level1: 0.9,
714 level2: 0.1,
715 },
716 UnitProbabilityJson {
717 level1: 0.6,
718 level2: 0.3,
719 },
720 UnitProbabilityJson {
721 level1: 0.4,
722 level2: 0.4,
723 },
724 UnitProbabilityJson {
725 level1: 0.2,
726 level2: 0.5,
727 },
728 UnitProbabilityJson {
729 level1: 0.1,
730 level2: 0.5,
731 },
732 UnitProbabilityJson {
733 level1: 0.1,
734 level2: 0.4,
735 },
736 UnitProbabilityJson {
737 level1: 0.1,
738 level2: 0.2,
739 },
740 UnitProbabilityJson {
741 level1: 0.1,
742 level2: 0.1,
743 },
744 ],
745 timeouts: [
746 (PhaseType::Lobby, 10_000),
747 (PhaseType::LightsaberShopPhase, 6_000),
748 (PhaseType::UnitShopPhase, 10_000),
749 (PhaseType::PlacementPhase, 20_000),
750 (PhaseType::FightPhase, 12_000),
751 ]
752 .into_iter()
753 .collect(),
754 liveLossOnDefeat: [(1, 1), (2, 10), (3, 20), (7, 15)].into_iter().collect(),
755 fieldModifiers: [
756 (
757 TileType::Force,
758 TileModifier {
759 health: 0,
760 armor: 0,
761 attack: 2,
762 },
763 ),
764 (
765 TileType::MedicCenter,
766 TileModifier {
767 health: 5,
768 armor: 0,
769 attack: 0,
770 },
771 ),
772 (
773 TileType::Lava,
774 TileModifier {
775 health: -3,
776 armor: 0,
777 attack: 0,
778 },
779 ),
780 ]
781 .into_iter()
782 .collect(),
783 }
784 }
785}