From e5b5e31f46a3f04fbae879f8e70c937b5029a827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20W=C3=B6lfer?= Date: Sun, 27 Jul 2025 20:34:40 +0200 Subject: [PATCH] Basic painting optimizer --- canvas/src/card.rs | 78 +++++++++++++++++++++++++---- canvas/src/icon_bitfield.rs | 71 ++++++++++++++------------- canvas/src/lib.rs | 82 ++++++++++++++++++++++++++++--- canvas/src/objective.rs | 7 ++- canvas/src/optimizer.rs | 97 +++++++++++++++++++++++++++++++++---- 5 files changed, 270 insertions(+), 65 deletions(-) diff --git a/canvas/src/card.rs b/canvas/src/card.rs index f38e3e3..1165929 100644 --- a/canvas/src/card.rs +++ b/canvas/src/card.rs @@ -1,19 +1,56 @@ use crate::{CardField, Icon}; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] // There are 60 cards in the base set pub struct Card { - pub field: [CardField; 5], + field: u32, +} + +pub struct CardFieldIter { + index: u8, + field: u32, +} + +impl Iterator for CardFieldIter { + type Item = CardField; + + fn next(&mut self) -> Option { + if self.index < 5 { + let old_index = self.index; + self.index += 1; + let bitmask = (1 << (CardField::BITPACKED_SIZE as u8)) - 1; + let result = (self.field >> (CardField::BITPACKED_SIZE as u8 * old_index)) & bitmask; + Some(CardField::from_bits(result as u8)) + } else { + None + } + } +} + +impl From<[CardField; 5]> for Card { + fn from(value: [CardField; 5]) -> Self { + let field = value + .iter() + .map(|x| x.to_bits()) + .zip((0u8..).step_by(CardField::BITPACKED_SIZE)) + .map(|(a, shift_count)| (a as u32) << (shift_count)) + .fold(0u32, |a, b| a | b); + Self { field } + } } impl Card { - pub const fn empty() -> Self { - Self { - field: [CardField::Empty; 5], + pub fn field_iter(&self) -> CardFieldIter { + CardFieldIter { + index: 0, + field: self.field, } } + pub const fn empty() -> Self { + Self { field: u32::MAX } + } pub fn from_dbg_string(text: &str) -> Self { - let mut result = Self::empty(); + let mut result = [CardField::Empty; 5]; let mut current_field = 0; let mut char_iter = text.chars(); let match_icon = |x: char| { @@ -28,7 +65,7 @@ impl Card { }; while let Some(c) = char_iter.next() { if let Some(f) = match_icon(c) { - result.field[current_field] = CardField::Icon(f); + result[current_field] = CardField::Icon(f); current_field += 1; continue; } @@ -48,16 +85,37 @@ impl Card { .expect("No second icon after double symbol"), ) .expect("Incorrect second icon after double symbol"); - CardField::DoubleIcon((f1, f2)) + CardField::DoubleIcon(f1, f2) } '.' => CardField::Empty, '-' => continue, _ => panic!("Unknown character {c}"), }; - result.field[current_field] = field; + result[current_field] = field; current_field += 1; } - result + result.into() + } +} + +#[cfg(test)] +mod tests { + use crate::{CardField, Icon, card::Card}; + + #[test] + fn test_correctness() { + let card = Card::from_dbg_string("l..t."); + println!("{:0x}", card.field); + assert_eq!( + card.field_iter().collect::>(), + vec![ + CardField::Icon(Icon::Color), + CardField::Empty, + CardField::Empty, + CardField::Icon(Icon::Triangle), + CardField::Empty, + ] + ) } } diff --git a/canvas/src/icon_bitfield.rs b/canvas/src/icon_bitfield.rs index 55c8268..7c7e6ab 100644 --- a/canvas/src/icon_bitfield.rs +++ b/canvas/src/icon_bitfield.rs @@ -38,7 +38,7 @@ impl IconBitfield { debug_assert!(index < 5); match field { CardField::Icon(icon) => self.add_icon(icon, index), - CardField::DoubleIcon((icon1, icon2)) => { + CardField::DoubleIcon(icon1, icon2) => { self.add_icon(icon1, index); self.add_icon(icon2, index); } @@ -50,8 +50,8 @@ impl IconBitfield { pub fn new(card: &Card) -> Self { let mut result = Self::default(); (0u8..) - .zip(card.field.iter()) - .for_each(|(index, field)| result.add_field(*field, index)); + .zip(card.field_iter()) + .for_each(|(index, field)| result.add_field(field, index)); result } @@ -84,35 +84,40 @@ impl IconBitfield { } } -#[test] -fn test_add_field() { - let mut x: IconBitfield = Default::default(); - x.add_icon(Icon::Circle, 2); - let expected = IconBitfield { - circle: 0b00100, - ..Default::default() - }; - assert_eq!(x, expected); -} +#[cfg(test)] +mod tests { + use crate::{Icon, icon_bitfield::IconBitfield}; -#[test] -fn test_layer_below() { - let mut a: IconBitfield = IconBitfield { - color: 0b00010, - points: 0b01000, - ..Default::default() - }; - let b = IconBitfield { - triangle: a.get_point_bits(), - square: 0b10000, - ..Default::default() - }; - let expected = IconBitfield { - color: a.get_icon_bits(Icon::Color), - points: a.get_point_bits(), - square: b.get_icon_bits(Icon::Square), - ..Default::default() - }; - a.layer_below(&b); - assert_eq!(a, expected); + #[test] + fn test_add_field() { + let mut x: IconBitfield = Default::default(); + x.add_icon(Icon::Circle, 2); + let expected = IconBitfield { + circle: 0b00100, + ..Default::default() + }; + assert_eq!(x, expected); + } + + #[test] + fn test_layer_below() { + let mut a: IconBitfield = IconBitfield { + color: 0b00010, + points: 0b01000, + ..Default::default() + }; + let b = IconBitfield { + triangle: a.get_point_bits(), + square: 0b10000, + ..Default::default() + }; + let expected = IconBitfield { + color: a.get_icon_bits(Icon::Color), + points: a.get_point_bits(), + square: b.get_icon_bits(Icon::Square), + ..Default::default() + }; + a.layer_below(&b); + assert_eq!(a, expected); + } } diff --git a/canvas/src/lib.rs b/canvas/src/lib.rs index 7b794a5..2381e56 100644 --- a/canvas/src/lib.rs +++ b/canvas/src/lib.rs @@ -1,4 +1,5 @@ #![allow(dead_code)] + use crate::objective::Objective; mod card; @@ -14,23 +15,88 @@ struct Shop { coins: [u8; 5], } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Icon { - Circle, - Square, - Triangle, - Color, + Circle = 0b00, + Square = 0b01, + Triangle = 0b10, + Color = 0b11, } -#[derive(Debug, Clone, Copy, Default)] -enum CardField { +impl TryFrom for Icon { + type Error = (); + + fn try_from(value: u8) -> Result { + ICON_ORDER + .get(value as usize) + .map(|x| Ok(*x)) + .unwrap_or(Err(())) + } +} +const ICON_ORDER: [Icon; 4] = [Icon::Circle, Icon::Square, Icon::Triangle, Icon::Color]; + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum CardField { Icon(Icon), - DoubleIcon((Icon, Icon)), PointsPer(Icon), + DoubleIcon(Icon, Icon), #[default] Empty, } +impl CardField { + const BITPACKED_SIZE: usize = 4; + fn from_bits(bits: u8) -> Self { + { + if bits == 0b1111 { + Self::Empty + } else if (bits & (1 << 3)) != 0 { + let bits = bits & ((1 << 3) - 1); + if bits == 5 { + Self::DoubleIcon(Icon::Triangle, Icon::Color) + } else if bits >= 3 { + Self::DoubleIcon(Icon::Square, Icon::try_from(bits + 2 - 3).unwrap()) + } else { + Self::DoubleIcon(Icon::Circle, Icon::try_from(bits + 1).unwrap()) + } + } else if bits & (1 << 2) != 0 { + let bits = bits & ((1 << 2) - 1); + Self::PointsPer(Icon::try_from(bits).unwrap()) + } else { + Self::Icon(Icon::try_from(bits).unwrap()) + } + } + } + /// There are only 4 + 4 + 6 + 1 possibilities when double icons are sorted + fn to_bits(self) -> u8 { + match self { + CardField::Icon(icon) => icon as u8, + CardField::PointsPer(icon) => icon as u8 | (1 << 2), + CardField::DoubleIcon(mut a, mut b) => { + debug_assert_ne!(a, b); + if a > b { + (a, b) = (b, a); + } + let tag = match a { + // 0..=2 + Icon::Circle => ICON_ORDER.iter().position(|x| b == *x).unwrap() as u8 - 1, + // 3..=4 + Icon::Square => ICON_ORDER.iter().position(|x| b == *x).unwrap() as u8 - 2 + 3, + // 5 + Icon::Triangle => { + debug_assert_eq!(b, Icon::Color); + 5 + } + Icon::Color => panic!("Color as first double is not possible"), + }; + tag | (1 << 3) + } + CardField::Empty => (1 << 4) - 1, + } + } +} + #[derive(Debug)] struct Player { cards: Vec, diff --git a/canvas/src/objective.rs b/canvas/src/objective.rs index 1226073..3a40e09 100644 --- a/canvas/src/objective.rs +++ b/canvas/src/objective.rs @@ -1,4 +1,4 @@ -use crate::{Icon, Painting, icon_bitfield::IconBitfield}; +use crate::{icon_bitfield::IconBitfield, Icon, Painting, ICON_ORDER}; // There are 12 objectives/criteria in the base set #[derive(Debug)] @@ -17,7 +17,6 @@ pub enum Objective { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IconSet(u8); -const ICON_SET_ORDER: [Icon; 4] = [Icon::Circle, Icon::Triangle, Icon::Square, Icon::Color]; pub struct IconSetIterator { icon_set: IconSet, @@ -28,7 +27,7 @@ impl From<&[Icon]> for IconSet { fn from(value: &[Icon]) -> Self { let v = value .iter() - .map(|x| 1 << ICON_SET_ORDER.iter().position(|v| x == v).unwrap()) + .map(|x| 1 << ICON_ORDER.iter().position(|v| x == v).unwrap()) .fold(0, |a, b| a | b); Self(v) } @@ -49,7 +48,7 @@ impl Iterator for IconSetIterator { self.index += 1; if self.icon_set.0 & (1 << old_index) != 0 { - return Some(*ICON_SET_ORDER.get(old_index as usize).unwrap()); + return Some(*ICON_ORDER.get(old_index as usize).unwrap()); } } None diff --git a/canvas/src/optimizer.rs b/canvas/src/optimizer.rs index 35d61e7..f238fe1 100644 --- a/canvas/src/optimizer.rs +++ b/canvas/src/optimizer.rs @@ -1,14 +1,91 @@ use itertools::Itertools; -use crate::{CriteriaCard, icon_bitfield::IconBitfield}; +use crate::{CardField, CriteriaCard, card::Card, icon_bitfield::IconBitfield}; -fn draw_painting(cards: &[IconBitfield], _obj: Vec<(&CriteriaCard, u8)>) -> [usize; 3] { - for stack in cards.iter().permutations(3) { - let stack: [&IconBitfield; 3] = stack.try_into().unwrap(); - let mut painting = stack[0].clone(); - painting.layer_below(stack[1]).layer_below(stack[2]); - // obj.iter() - // .map(|x| x.0.objective.count_matches_bitfield(&painting)); - } - todo!() +struct OneIndexIterator { + n: u64, + offset: u8, +} + +impl From for OneIndexIterator { + fn from(value: u64) -> Self { + Self { + n: value, + offset: 0, + } + } +} + +impl Iterator for OneIndexIterator { + type Item = u8; + + fn next(&mut self) -> Option { + if self.n == 0 { + return None; + } + let new_offset = self.n.trailing_zeros() as u8; + self.n >>= new_offset + 1; + let result = self.offset + new_offset; + self.offset += new_offset + 1; + Some(result) + } +} + +fn draw_painting(cards: &[Card], obj: &[(&CriteriaCard, u8)]) -> Option<[usize; 3]> { + let bitfields = cards.iter().map(IconBitfield::new).collect::>(); + let mut best: Option<([usize; 3], u8)> = None; + for stack in (0..cards.len()).permutations(3) { + let stack_indices: [_; 3] = stack.try_into().unwrap(); + let stack = stack_indices.map(|x| cards[x]); + let maps = stack_indices.map(|x| &bitfields[x]); + let mut painting = maps[0].clone(); + painting.layer_below(maps[1]).layer_below(maps[2]); + let criteria_points = obj + .iter() + .map(|(criteria, reached)| { + criteria.points.get(usize::from( + criteria.objective.count_matches_bitfield(&painting) + *reached, + )) + }) + .sum::(); + let bonus_points = OneIndexIterator::from(u64::from(painting.get_point_bits())) + .map(|bit_index| { + let matched_icon = stack + .iter() + .map(|x| x.field_iter().nth(usize::from(bit_index)).unwrap()) + .find_map(|x| match x { + CardField::PointsPer(icon) => Some(icon), + _ => None, + }) + .expect("Point bits lied"); + painting.get_icon_bits(matched_icon).count_ones() as u8 * 2 + }) + .sum::(); + let total_points = bonus_points + criteria_points; + let stack_result = (stack_indices, total_points); + dbg!(stack_result); + if let Some(bp) = &mut best { + if bp.1 < total_points { + *bp = stack_result; + } + } else { + best = Some(stack_result) + } + } + best.map(|x| x.0) +} + +#[cfg(test)] +mod tests { + use crate::{card::Card, optimizer::draw_painting}; + + #[test] + fn test_basic() { + let cards = vec![ + Card::from_dbg_string(".ttt."), + Card::from_dbg_string("t.l.."), + Card::from_dbg_string("ll..pt"), + ]; + assert_eq!(draw_painting(&cards, &[]), Some([0, 1, 2])); + } }