Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kai"
version = "0.21.2"
version = "0.21.3"
edition = "2021"
default-run = "kai"

Expand Down
11 changes: 11 additions & 0 deletions src/ai/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -880,11 +880,22 @@ pub fn load_deck(seat: &mut Seat, path: &str, out: &mut Out) {
}
}

pub fn deal_rules_enforced(options: Option<&[u8]>) -> bool {
agni_riftbound::TableOptions::enforced_in(options)
}

pub fn deal(seat: &mut Seat, link: &mut dyn Link, out: &mut Out) {
let Some(record) = seat.deck.as_mut() else {
out.line("no deck loaded — `deck <file>` first");
return;
};
let enforced = seat.session.as_ref().is_some_and(|session| {
deal_rules_enforced(session.state().options.as_ref().map(|options| &options[..]))
});
if let Err(error) = crate::deck::actions::validate_deal(&record.deck, enforced) {
out.line(format!("deck refused: {error}"));
return;
}
record.seat = PlayerId(seat.seat);
if crate::deck::battlefield::needs_choice(record) {
out.line("choose a battlefield first: `battlefield <n>`");
Expand Down
78 changes: 69 additions & 9 deletions src/deck/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ pub fn search_cards(cards: &Catalog, query: &str, offset: usize) -> Value {

pub fn inspect(draft: &Draft) -> Value {
let deck = ImportedDeck::Riftbound(draft.deck.clone());
let report = deck
.report()
.expect("Riftbound decks have legality reports");
json!({"label": draft.label, "summary": deck.summary(),
"list": super::exchange::render(&draft.deck, super::exchange::Share::TextList).unwrap_or_default(),
"validation": super::import::findings_text(&draft.report), "unresolved": draft.unresolved})
"validation": super::import::findings_text(&report), "unresolved": draft.unresolved})
}

pub fn edit(draft: &mut Option<Draft>, cards: &Catalog, args: &Value) -> Result<Value, String> {
Expand Down Expand Up @@ -162,15 +165,24 @@ pub fn selection_legal(verdict: agni_riftbound::legality::Verdict, enforced: boo
!enforced || !matches!(verdict, agni_riftbound::legality::Verdict::Broken(_))
}

pub fn validate_selection(draft: &Draft, enforced: bool, confirmed: bool) -> Result<(), String> {
if !selection_legal(draft.report.verdict, enforced) {
return Err(super::import::findings_text(&draft.report));
pub fn validate_deal(deck: &ImportedDeck, enforced: bool) -> Result<(), String> {
let Some(report) = deck.report() else {
return Ok(());
};
if selection_legal(report.verdict, enforced) {
Ok(())
} else {
Err(super::import::findings_text(&report))
}
if matches!(
draft.report.verdict,
agni_riftbound::legality::Verdict::Broken(_)
) && !confirmed
{
}

pub fn validate_selection(draft: &Draft, enforced: bool, confirmed: bool) -> Result<(), String> {
let deck = ImportedDeck::Riftbound(draft.deck.clone());
validate_deal(&deck, enforced)?;
let report = deck
.report()
.expect("Riftbound decks have legality reports");
if matches!(report.verdict, agni_riftbound::legality::Verdict::Broken(_)) && !confirmed {
return Err(
"This free-table deck is invalid. Confirm explicitly to seat it anyway.".into(),
);
Expand Down Expand Up @@ -304,4 +316,52 @@ mod tests {
.unwrap()
.is_empty());
}

fn entry(name: &str, id: &str) -> agni_riftbound::DeckEntry {
agni_riftbound::DeckEntry {
card: agni_riftbound::ResolvedCard {
name: name.into(),
riftbound_id: id.into(),
..Default::default()
},
count: 1,
}
}

#[test]
fn selection_and_deals_recheck_banned_sideboards() {
let mut deck = crate::deck::pool::deck("lillia-house").unwrap();
deck.sideboard = vec![entry("Stacked Deck", "ogn-183-298")];
let imported = ImportedDeck::Riftbound(deck.clone());
let report = imported.report().unwrap();
assert!(report.findings.iter().any(|finding| {
matches!(
&finding.rule,
agni_riftbound::legality::Rule::Banned { name } if name == "Stacked Deck"
) && finding.zone == agni_riftbound::legality::Zone::Sideboard
}));
assert!(validate_deal(&imported, true).is_err());
assert!(validate_deal(&imported, false).is_ok());

let mut draft = Draft::from_deck(
crate::deck::pool::deck("lillia-house").unwrap(),
"stale",
Origin::New,
);
draft.deck = deck;
assert!(validate_selection(&draft, true, false).is_err());
assert!(validate_selection(&draft, false, false).is_err());
assert!(validate_selection(&draft, false, true).is_ok());
}

#[test]
fn other_titles_stay_available_to_enforced_deals() {
let mut deck = crate::deck::pool::deck("lillia-house").unwrap();
deck.main_deck[0].card.name = "Ekko - Another Title".into();
let imported = ImportedDeck::Riftbound(deck);
assert!(imported.report().unwrap().findings.iter().all(|finding| {
!matches!(&finding.rule, agni_riftbound::legality::Rule::Banned { .. })
}));
assert!(validate_deal(&imported, true).is_ok());
}
}
14 changes: 11 additions & 3 deletions src/deck/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@ impl ImportedDeck {
cards
}

pub fn report(&self) -> Option<agni_riftbound::legality::Report> {
match self {
Self::Riftbound(deck) => Some(agni_riftbound::legality::check(
deck,
agni_riftbound::legality::Mode::Standard,
)),
Self::Mtg(_) => None,
}
}

pub fn summary(&self) -> Vec<String> {
match self {
Self::Riftbound(deck) => {
Expand Down Expand Up @@ -168,9 +178,7 @@ impl ResolvedImport {
}

pub fn report(&self) -> Option<agni_riftbound::legality::Report> {
self.riftbound().map(|deck| {
agni_riftbound::legality::check(deck, agni_riftbound::legality::Mode::Standard)
})
self.deck.report()
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/deck/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,24 @@ mod tests {
}
let report =
agni_riftbound::legality::check(&deck, agni_riftbound::legality::Mode::Standard);
if held.slug == "kha-zix-hotkee" {
assert_eq!(
deck.main_deck
.iter()
.find(|entry| entry.card.name == "Stacked Deck")
.map(|entry| entry.count),
Some(3)
);
assert_eq!(report.breaks(), 1, "{:?}", report.findings);
assert_eq!(report.verdict, agni_riftbound::legality::Verdict::Broken(1));
assert!(report.findings.iter().any(|finding| {
matches!(
&finding.rule,
agni_riftbound::legality::Rule::Banned { name } if name == "Stacked Deck"
)
}));
continue;
}
assert_eq!(report.breaks(), 0, "{}: {:?}", held.slug, report.findings);
assert_eq!(
report.verdict,
Expand Down
17 changes: 15 additions & 2 deletions src/menu/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ pub fn editor_screen(
}
let now = ui.input(|input| input.time);
for action in actions {
run_action(action, now, menu, my_seat, decks);
run_action(action, now, enforced, menu, my_seat, decks);
}
}

Expand Down Expand Up @@ -1408,7 +1408,14 @@ pub fn browser_edit(draft: &Draft, catalog: &Catalog, action: BrowserAction) ->
})
}

fn run_action(action: Action, now: f64, menu: &mut Menu, my_seat: &MySeat, decks: &mut DeckParams) {
fn run_action(
action: Action,
now: f64,
enforced: bool,
menu: &mut Menu,
my_seat: &MySeat,
decks: &mut DeckParams,
) {
let game_tag = agni_riftbound::GAME.to_string();
let DeckParams {
editor,
Expand Down Expand Up @@ -1515,6 +1522,12 @@ fn run_action(action: Action, now: f64, menu: &mut Menu, my_seat: &MySeat, decks
editor::close(editor, menu);
}
Action::Seat { next_game } => {
if let Err(error) =
crate::deck::actions::validate_selection(draft, enforced, sheet.seat_armed)
{
sheet.status(error, now);
return;
}
let saved = if cfg!(target_arch = "wasm32") {
Err(WEB_SAVE_NOTE.to_string())
} else {
Expand Down
Loading
Loading