1use super::{resolve_numeric_svar, EffectContext};
2use crate::agent::PlayerAgent;
3use crate::game::GameState;
4use crate::ids::PlayerId;
5use crate::parsing::keys;
6use crate::parsing::Params;
7use crate::spellability::{build_spell_ability, SpellAbility};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, strum_macros::EnumString)]
13#[strum(ascii_case_insensitive)]
14pub enum ChoiceRestriction {
15 ThisGame,
16 ThisTurn,
17 YourLastCombat,
18}
19
20#[manabrew_engine_macros::spell_effect(CharmEffect)]
35fn resolve(ctx: &mut EffectContext, sa: &crate::spellability::SpellAbility) {
36 if sa.sub_ability.is_some() {
41 return;
42 }
43
44 let source_id = match sa.source {
45 Some(id) => id,
46 None => return,
47 };
48
49 let choices_str = sa.ir.choices.clone().unwrap_or_default();
50 if choices_str.is_empty() {
51 return;
52 }
53
54 let charm_num = resolve_numeric_svar(ctx.game, sa, keys::CHARM_NUM, 1).max(0) as usize;
55 let min_charm_num =
56 resolve_numeric_svar(ctx.game, sa, keys::MIN_CHARM_NUM, charm_num as i32).max(0) as usize;
57
58 let player = sa.activating_player;
59
60 let mode_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
62
63 let svars = ctx.game.card(source_id).svars.clone();
65 let mode_texts: Vec<String> = mode_svars
66 .iter()
67 .filter_map(|svar| svars.get(*svar).cloned())
68 .collect();
69
70 if mode_texts.is_empty() {
71 return;
72 }
73
74 let mode_descriptions: Vec<String> = mode_texts
76 .iter()
77 .map(|text| {
78 let params = Params::from_raw(text);
79 params
80 .get_cloned(keys::SPELL_DESCRIPTION)
81 .unwrap_or_else(|| text.clone())
82 })
83 .collect();
84
85 let restriction = sa
90 .ir
91 .choice_restriction_text
92 .as_deref()
93 .and_then(|s| s.parse::<ChoiceRestriction>().ok());
94 let current_turn = ctx.game.turn.turn_number as i32;
95 let last_combat_turn = ctx
96 .game
97 .last_combat_turn_of(sa.activating_player)
98 .unwrap_or(i32::MIN);
99 let is_restricted_index = |mode_svar: &str| -> bool {
100 let Some(scope) = restriction else {
101 return false;
102 };
103 let history = &ctx.game.card(source_id).chosen_charm_modes;
104 let Some(&turn) = history.get(mode_svar) else {
105 return false;
106 };
107 match scope {
108 ChoiceRestriction::ThisGame => true,
109 ChoiceRestriction::ThisTurn => turn == current_turn,
110 ChoiceRestriction::YourLastCombat => turn >= last_combat_turn,
111 }
112 };
113
114 let valid_mode_indices: Vec<usize> = mode_texts
117 .iter()
118 .enumerate()
119 .filter(|(i, text)| {
120 !is_restricted_index(mode_svars[*i])
121 && mode_has_valid_targets(ctx, text, player, source_id)
122 })
123 .map(|(i, _)| i)
124 .collect();
125
126 if valid_mode_indices.is_empty() {
127 return; }
129
130 let valid_descriptions: Vec<String> = valid_mode_indices
131 .iter()
132 .map(|&i| mode_descriptions[i].clone())
133 .collect();
134
135 let entwine_paid = sa.ir.entwine || sa.kicked; let has_entwine = ctx.game.card(source_id).get_entwine_cost().is_some();
140 let has_escalate = ctx.game.card(source_id).get_escalate_cost().is_some();
141
142 let charm_num = if has_escalate {
144 mode_texts.len()
145 } else {
146 charm_num
147 };
148
149 let use_preselected_modes = should_use_preselected_modes(ctx.game, source_id, &mode_texts);
151 let pre_selected = if use_preselected_modes {
153 ctx.game.card_mut(source_id).chosen_modes.take()
154 } else {
155 ctx.game.card_mut(source_id).chosen_modes = None;
156 None
157 };
158 let mut chosen_indices: Vec<usize> = if let Some(pre) = pre_selected {
159 pre
161 } else if entwine_paid || (has_entwine && sa.kicked) {
162 valid_mode_indices.clone()
164 } else {
165 let agent_choices = ctx.agents[player.index()].choose_mode(
166 player,
167 &valid_descriptions,
168 min_charm_num,
169 charm_num.min(valid_mode_indices.len()),
170 Some(source_id),
171 );
172 agent_choices
174 .into_iter()
175 .filter_map(|i| valid_mode_indices.get(i).copied())
176 .collect()
177 };
178
179 chosen_indices.sort();
184
185 for &idx in &chosen_indices {
189 if let Some(svar_name) = mode_svars.get(idx).copied() {
190 ctx.game
191 .card_mut(source_id)
192 .chosen_charm_modes
193 .insert(svar_name.to_string(), current_turn);
194 }
195 }
196
197 for idx in chosen_indices {
199 if idx >= mode_texts.len() {
200 continue;
201 }
202 let mode_text = &mode_texts[idx];
203
204 let mut mode_sa = build_spell_ability(ctx.game, source_id, mode_text, player);
206 mode_sa.source = Some(source_id);
207 mode_sa.trigger_remembered_amount = sa.trigger_remembered_amount;
210
211 let mut cur_opt: Option<SpellAbility> = Some(mode_sa);
213 while let Some(mut cur_sa) = cur_opt {
214 setup_mode_targets(ctx, &mut cur_sa, player);
215 super::resolve_effect(ctx, &cur_sa);
216 cur_opt = cur_sa.sub_ability.map(|b| *b);
218 if ctx.game.game_over {
219 break;
220 }
221 }
222
223 if ctx.game.game_over {
224 break;
225 }
226 }
227}
228
229pub fn make_choices_precast(
230 game: &mut GameState,
231 agents: &mut [Box<dyn PlayerAgent>],
232 sa: &mut SpellAbility,
233) -> bool {
234 make_choices_precast_with_count(game, agents, sa).is_some()
235}
236
237pub fn make_choices_precast_with_count(
238 game: &mut GameState,
239 agents: &mut [Box<dyn PlayerAgent>],
240 sa: &mut SpellAbility,
241) -> Option<usize> {
242 let source_id = match sa.source {
243 Some(id) => id,
244 None => return Some(0),
245 };
246
247 let choices_str = sa.ir.choices.clone().unwrap_or_default();
248 if choices_str.is_empty() {
249 return Some(0);
250 }
251
252 let player = sa.activating_player;
253 let mode_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
254 let svars = game.card(source_id).svars.clone();
255 let mode_texts: Vec<String> = mode_svars
256 .iter()
257 .filter_map(|svar| svars.get(*svar).cloned())
258 .collect();
259 if mode_texts.is_empty() {
260 return None;
261 }
262
263 let mode_descriptions: Vec<String> = mode_texts
264 .iter()
265 .map(|text| {
266 let params = Params::from_raw(text);
267 params
268 .get_cloned(keys::SPELL_DESCRIPTION)
269 .unwrap_or_else(|| text.clone())
270 })
271 .collect();
272
273 let restriction = sa
278 .ir
279 .choice_restriction_text
280 .as_deref()
281 .and_then(|s| s.parse::<ChoiceRestriction>().ok());
282 let current_turn = game.turn.turn_number as i32;
283 let last_combat_turn = game.last_combat_turn_of(player).unwrap_or(i32::MIN);
284 let is_restricted_index = |mode_svar: &str| -> bool {
285 let Some(scope) = restriction else {
286 return false;
287 };
288 let history = &game.card(source_id).chosen_charm_modes;
289 let Some(&turn) = history.get(mode_svar) else {
290 return false;
291 };
292 match scope {
293 ChoiceRestriction::ThisGame => true,
294 ChoiceRestriction::ThisTurn => turn == current_turn,
295 ChoiceRestriction::YourLastCombat => turn >= last_combat_turn,
296 }
297 };
298
299 let valid_mode_indices: Vec<usize> = mode_texts
300 .iter()
301 .enumerate()
302 .filter(|(i, text)| {
303 !is_restricted_index(mode_svars[*i])
304 && mode_has_valid_targets_in_game(game, text, player, source_id)
305 })
306 .map(|(i, _)| i)
307 .collect();
308 if valid_mode_indices.is_empty() {
309 return None;
310 }
311
312 let valid_descriptions: Vec<String> = valid_mode_indices
313 .iter()
314 .map(|&i| mode_descriptions[i].clone())
315 .collect();
316
317 let has_entwine = game.card(source_id).get_entwine_cost().is_some();
318 let has_escalate = game.card(source_id).get_escalate_cost().is_some();
319 let can_repeat = sa.ir.can_repeat_modes;
320
321 let mut charm_num = resolve_numeric_svar(game, sa, keys::CHARM_NUM, 1).max(0) as usize;
322 let min_charm_num =
323 resolve_numeric_svar(game, sa, keys::MIN_CHARM_NUM, charm_num as i32).max(0) as usize;
324 if has_escalate {
325 charm_num = mode_texts.len();
326 }
327 if !can_repeat && min_charm_num > valid_mode_indices.len() {
328 return None;
329 }
330
331 let use_preselected_modes = should_use_preselected_modes(game, source_id, &mode_texts);
332 let pre_selected = if use_preselected_modes {
333 game.card_mut(source_id).chosen_modes.take()
334 } else {
335 game.card_mut(source_id).chosen_modes = None;
336 None
337 };
338 let mut chosen_indices: Vec<usize> = if let Some(pre) = pre_selected {
339 pre
340 } else if sa.ir.entwine || (has_entwine && sa.kicked) {
341 valid_mode_indices.clone()
342 } else {
343 let chosen = agents[player.index()].choose_mode(
344 player,
345 &valid_descriptions,
346 min_charm_num,
347 charm_num.min(valid_mode_indices.len()),
348 Some(source_id),
349 );
350 chosen
351 .into_iter()
352 .filter_map(|i| valid_mode_indices.get(i).copied())
353 .collect()
354 };
355
356 if chosen_indices.len() < min_charm_num {
357 return None;
358 }
359
360 chosen_indices.sort();
365 let selected_mode_count = chosen_indices.len();
366
367 for &idx in &chosen_indices {
374 if let Some(svar_name) = mode_svars.get(idx).copied() {
375 game.card_mut(source_id)
376 .chosen_charm_modes
377 .insert(svar_name.to_string(), current_turn);
378 }
379 }
380
381 sa.sub_ability = None;
382 let parent_trigger_remembered = sa.trigger_remembered_amount;
383 for idx in chosen_indices {
384 if idx >= mode_texts.len() {
385 continue;
386 }
387 let mut mode_sa = build_spell_ability(game, source_id, &mode_texts[idx], player);
388 mode_sa.source = Some(source_id);
389 mode_sa.trigger_remembered_amount = parent_trigger_remembered;
392 append_subability(sa, mode_sa);
393 }
394
395 Some(selected_mode_count)
396}
397
398pub(crate) fn can_make_choices_precast(
403 game: &GameState,
404 player: PlayerId,
405 source_id: crate::ids::CardId,
406 charm_sa_text: &str,
407) -> bool {
408 let sa_params = Params::from_raw(charm_sa_text);
409 let Some(choices_str) = sa_params.get(keys::CHOICES) else {
410 return true;
411 };
412
413 let mode_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
414 if mode_svars.is_empty() {
415 return false;
416 }
417
418 let svars = game.card(source_id).svars.clone();
419 let mode_texts: Vec<String> = mode_svars
420 .iter()
421 .filter_map(|svar| svars.get(*svar).cloned())
422 .collect();
423 if mode_texts.is_empty() {
424 return false;
425 }
426
427 let mut charm_num: usize = sa_params
428 .get(keys::CHARM_NUM)
429 .and_then(|s| {
430 s.parse().ok().or_else(|| {
431 game.card(source_id)
433 .svars
434 .get(s.trim())
435 .and_then(|v| v.parse().ok())
436 })
437 })
438 .unwrap_or(1);
439 let min_charm_num: usize = sa_params
440 .get(keys::MIN_CHARM_NUM)
441 .and_then(|s| {
442 s.parse().ok().or_else(|| {
443 game.card(source_id)
444 .svars
445 .get(s.trim())
446 .and_then(|v| v.parse().ok())
447 })
448 })
449 .unwrap_or(charm_num);
450 let can_repeat = sa_params.has(keys::CAN_REPEAT_MODES);
451
452 let valid_count = mode_texts
453 .iter()
454 .filter(|text| mode_has_valid_targets_in_game(game, text, player, source_id))
455 .count();
456
457 if valid_count == 0 {
458 return false;
459 }
460
461 if !can_repeat && min_charm_num > valid_count {
462 return false;
463 }
464
465 if !can_repeat {
466 charm_num = charm_num.min(valid_count);
467 }
468
469 charm_num >= min_charm_num
470}
471
472pub fn make_possible_options(
477 game: &GameState,
478 source_id: crate::ids::CardId,
479 player: PlayerId,
480 choices_str: &str,
481) -> Vec<usize> {
482 let mode_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
483 let svars = game.card(source_id).svars.clone();
484 let mode_texts: Vec<String> = mode_svars
485 .iter()
486 .filter_map(|svar| svars.get(*svar).cloned())
487 .collect();
488
489 mode_texts
490 .iter()
491 .enumerate()
492 .filter(|(_, text)| mode_has_valid_targets_in_game(game, text, player, source_id))
493 .map(|(i, _)| i)
494 .collect()
495}
496
497pub fn make_formated_description(
502 game: &GameState,
503 source_id: crate::ids::CardId,
504 choices_str: &str,
505) -> String {
506 let mode_svars: Vec<&str> = choices_str.split(',').map(|s| s.trim()).collect();
507 let svars = game.card(source_id).svars.clone();
508 let mode_texts: Vec<String> = mode_svars
509 .iter()
510 .filter_map(|svar| svars.get(*svar).cloned())
511 .collect();
512
513 let mut description = String::new();
514 description.push_str("Choose one —\n");
515 for (i, text) in mode_texts.iter().enumerate() {
516 let params = Params::from_raw(text);
517 let mode_desc = params
518 .get_cloned(keys::SPELL_DESCRIPTION)
519 .unwrap_or_else(|| text.clone());
520 description.push_str(&format!("• {}\n", mode_desc));
521 let _ = i; }
523 description
524}
525
526pub fn make_choices(
531 game: &mut GameState,
532 agents: &mut [Box<dyn PlayerAgent>],
533 sa: &mut SpellAbility,
534) -> bool {
535 make_choices_precast(game, agents, sa)
536}
537
538pub fn chain_abilities(
543 game: &GameState,
544 sa: &mut SpellAbility,
545 mode_texts: &[String],
546 player: PlayerId,
547 source_id: crate::ids::CardId,
548) {
549 for mode_text in mode_texts {
550 let mut mode_sa = build_spell_ability(game, source_id, mode_text, player);
551 mode_sa.source = Some(source_id);
552 mode_sa.trigger_remembered_amount = sa.trigger_remembered_amount;
553 append_subability(sa, mode_sa);
554 }
555}
556
557fn mode_has_valid_targets(
564 ctx: &EffectContext,
565 mode_text: &str,
566 player: PlayerId,
567 source_id: crate::ids::CardId,
568) -> bool {
569 mode_has_valid_targets_in_game(ctx.game, mode_text, player, source_id)
570}
571
572fn append_subability(root: &mut SpellAbility, mode_sa: SpellAbility) {
573 let mut slot = &mut root.sub_ability;
574 loop {
575 match slot {
576 Some(node) => slot = &mut node.sub_ability,
577 None => {
578 *slot = Some(Box::new(mode_sa));
579 return;
580 }
581 }
582 }
583}
584
585fn should_use_preselected_modes(
586 game: &GameState,
587 source_id: crate::ids::CardId,
588 mode_texts: &[String],
589) -> bool {
590 let source = game.card(source_id);
591 if source.has_keyword("Spree") || source.has_keyword("Tiered") {
592 return true;
593 }
594
595 mode_texts.iter().any(|text| {
596 let params = Params::from_raw(text);
597 params.has(keys::MODE_COST)
598 })
599}
600
601pub(crate) fn mode_has_valid_targets_in_game(
602 game: &GameState,
603 mode_text: &str,
604 player: PlayerId,
605 source_id: crate::ids::CardId,
606) -> bool {
607 let sa = build_spell_ability(game, source_id, mode_text, player);
608 let tr = match &sa.target_restrictions {
609 Some(tr) => tr,
610 None => return true, };
612
613 if tr.get_min_targets(game, &sa) <= 0 {
617 return true;
618 }
619
620 tr.has_candidates(game, player, sa.source)
621}
622
623fn setup_mode_targets(ctx: &mut EffectContext, mode_sa: &mut SpellAbility, player: PlayerId) {
624 if !mode_sa.uses_targeting() {
625 return;
626 }
627 mode_sa.targeting_player = Some(player);
628 ctx.agents[player.index()].choose_targets_for(mode_sa, ctx.game, ctx.mana_pools);
629}
630
631#[cfg(test)]
632mod tests {
633 use crate::ability::spell_ability_effect::SpellAbilityEffect;
634 use forge_foundation::{CardTypeLine, ColorSet, ManaCost, ZoneType};
635 use std::collections::{BTreeMap, HashMap};
636
637 use crate::ability::effects::EffectContext;
638 use crate::agent::PassAgent;
639 use crate::card::Card;
640 use crate::game::GameState;
641 use crate::ids::{CardId, PlayerId};
642 use crate::mana::ManaPool;
643 use crate::spellability::SpellAbility;
644 use crate::trigger::handler::TriggerHandler;
645
646 #[test]
647 fn charm_choose_mode_zero_choices_noops() {
648 let mut game = GameState::new(&["Alice", "Bob"], 20);
649 let p0 = PlayerId(0);
650
651 let sa = SpellAbility::new_simple(None, p0, "A:SP$ Charm");
653 let mut th = TriggerHandler::new();
654 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
655 vec![Box::new(PassAgent), Box::new(PassAgent)];
656 let mut mp = vec![ManaPool::default(), ManaPool::default()];
657 let templates = HashMap::new();
658 let templates_variants = HashMap::new();
659 let token_fallback = HashMap::new();
660 let edition_dates: HashMap<String, String> = HashMap::new();
661 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
662 let mut ctx = EffectContext {
663 game: &mut game,
664 combat: None,
665 agents: &mut agents,
666 trigger_handler: &mut th,
667 token_templates: &templates,
668 token_art_variants: &templates_variants,
669 token_fallback: &token_fallback,
670 edition_dates: &edition_dates,
671 mana_pools: &mut mp,
672 parent_target_card: None,
673 rng: &mut rng_adapter,
674 };
675 super::CharmEffect::resolve(&mut ctx, &sa);
677 }
678
679 #[test]
682 fn charm_resolves_chosen_non_targeted_mode() {
683 let mut game = GameState::new(&["Alice", "Bob"], 20);
684 let p0 = PlayerId(0);
685 let p1 = PlayerId(1);
686
687 let mut svars = BTreeMap::new();
689 svars.insert(
691 "ModeA".to_string(),
692 "DB$ Draw | NumCards$ 1 | Defined$ You | SpellDescription$ Draw a card.".to_string(),
693 );
694 svars.insert(
696 "ModeB".to_string(),
697 "DB$ Draw | NumCards$ 1 | Defined$ Opponent | SpellDescription$ Opponent draws."
698 .to_string(),
699 );
700
701 let charm_card = Card::new(
702 CardId(0),
703 "Test Charm".into(),
704 p0,
705 CardTypeLine::parse("Instant"),
706 ManaCost::parse("U B"),
707 ColorSet::from_names("u"),
708 None,
709 None,
710 vec!["A:SP$ Charm | Choices$ ModeA,ModeB".to_string()],
711 vec![],
712 );
713 let cid = game.create_card(charm_card);
715 game.card_mut(cid).set_svars_map(svars);
716
717 let dummy_a = game.create_card(Card::new(
719 CardId(0),
720 "Dummy A".into(),
721 p0,
722 CardTypeLine::parse("Creature"),
723 ManaCost::parse(""),
724 ColorSet::COLORLESS,
725 Some(1),
726 Some(1),
727 vec![],
728 vec![],
729 ));
730 game.move_card(dummy_a, ZoneType::Library, p0);
731
732 let sa = SpellAbility::new_simple(Some(cid), p0, "A:SP$ Charm | Choices$ ModeA,ModeB");
733
734 let mut th = TriggerHandler::new();
735 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
736 vec![Box::new(PassAgent), Box::new(PassAgent)];
737 let mut mp = vec![ManaPool::default(), ManaPool::default()];
738 let templates = HashMap::new();
739 let templates_variants = HashMap::new();
740 let token_fallback = HashMap::new();
741 let edition_dates: HashMap<String, String> = HashMap::new();
742 let mut rng_adapter = crate::game_rng::ThreadRngAdapter;
743 let mut ctx = EffectContext {
744 game: &mut game,
745 combat: None,
746 agents: &mut agents,
747 trigger_handler: &mut th,
748 token_templates: &templates,
749 token_art_variants: &templates_variants,
750 token_fallback: &token_fallback,
751 edition_dates: &edition_dates,
752 mana_pools: &mut mp,
753 parent_target_card: None,
754 rng: &mut rng_adapter,
755 };
756
757 let p0_hand_before = ctx.game.cards_in_zone(ZoneType::Hand, p0).len();
759 super::CharmEffect::resolve(&mut ctx, &sa);
760 let p0_hand_after = ctx.game.cards_in_zone(ZoneType::Hand, p0).len();
761 assert_eq!(p0_hand_after, p0_hand_before + 1);
763 assert_eq!(ctx.game.cards_in_zone(ZoneType::Hand, p1).len(), 0);
765 }
766
767 #[test]
768 fn charm_precast_does_not_reuse_stale_chosen_modes() {
769 let mut game = GameState::new(&["Alice", "Bob"], 20);
770 let p0 = PlayerId(0);
771 let p1 = PlayerId(1);
772
773 let mut svars = BTreeMap::new();
774 svars.insert(
775 "BraveTheStench".to_string(),
776 "DB$ Pump | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls. | NumAtt$ -1 | NumDef$ -1 | IsCurse$ True | SpellDescription$ Brave the Stench".to_string(),
777 );
778 svars.insert(
779 "SearchTheBody".to_string(),
780 "DB$ Token | TokenScript$ c_a_treasure_sac | TokenOwner$ You | SpellDescription$ Search the Body".to_string(),
781 );
782
783 let ghast = Card::new(
784 CardId(0),
785 "Shambling Ghast".into(),
786 p1,
787 CardTypeLine::parse("Creature Zombie"),
788 ManaCost::parse("B"),
789 ColorSet::from_names("b"),
790 Some(1),
791 Some(1),
792 vec!["A:SP$ Charm | Choices$ BraveTheStench,SearchTheBody".to_string()],
793 vec![],
794 );
795 let ghast_id = game.create_card(ghast);
796 game.card_mut(ghast_id).set_svars_map(svars);
797
798 let patient_zero = game.create_card(Card::new(
799 CardId(0),
800 "Patient Zero".into(),
801 p0,
802 CardTypeLine::parse("Creature Zombie"),
803 ManaCost::parse("1 B"),
804 ColorSet::from_names("b"),
805 Some(2),
806 Some(2),
807 vec![],
808 vec![],
809 ));
810 game.move_card(patient_zero, ZoneType::Battlefield, p0);
811
812 let mut sa = SpellAbility::new_simple(
813 Some(ghast_id),
814 p1,
815 "A:SP$ Charm | Choices$ BraveTheStench,SearchTheBody",
816 );
817 game.card_mut(ghast_id).set_chosen_modes(vec![1]);
819 let mut agents: Vec<Box<dyn crate::agent::PlayerAgent>> =
820 vec![Box::new(PassAgent), Box::new(PassAgent)];
821
822 assert!(super::make_choices_precast(&mut game, &mut agents, &mut sa));
823 assert!(game.card(ghast_id).chosen_modes.is_none());
824 assert_eq!(
825 sa.sub_ability.as_ref().and_then(|sub| sub.api),
826 Some(crate::ability::api_type::ApiType::Pump)
827 );
828 }
829}