use std::cmp::{Ordering, PartialOrd};
use std::fmt::{Debug, Display};
use std::iter::FusedIterator;
use std::ops::{
BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Sub, SubAssign,
};
use serde::{Deserialize, Serialize};
use crate::ally::Ally;
#[derive(Clone, Copy, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[must_use]
pub struct AllySet(u16);
macro_rules! ally_bits {
($($ally:ident),+) => {
$(crate::ally::Ally::$ally.to_bit_mask()) | +
};
}
impl AllySet {
const EVERYONE_BITS: u16 = (ally_bits!(Morinth) << 1) - 1;
const IDEAL_BIOTICS_BITS: u16 = ally_bits!(Jack, Samara, Morinth);
const IDEAL_TECHS_BITS: u16 = ally_bits!(Kasumi, Legion, Tali);
const OPTIONAL_BITS: u16 = Self::EVERYONE_BITS & !Self::REQUIRED_BITS;
const REQUIRED_BITS: u16 = ally_bits!(Garrus, Jack, Jacob, Miranda, Mordin);
pub const NOBODY: Self = Self(0);
pub const EVERYONE: Self = Self(Self::EVERYONE_BITS);
pub const REQUIRED: Self = Self(Self::REQUIRED_BITS);
pub const OPTIONAL: Self = Self(Self::OPTIONAL_BITS);
pub const RECRUITABLE: Self = Self(Self::OPTIONAL_BITS & !ally_bits!(Morinth));
pub const ASARI: Self = Self(ally_bits!(Morinth, Samara));
pub const IDEAL_LEADERS: Self = Self(ally_bits!(Garrus, Jacob, Miranda));
pub const IDEAL_TECHS: Self = Self(Self::IDEAL_TECHS_BITS);
pub const IDEAL_BIOTICS: Self = Self(Self::IDEAL_BIOTICS_BITS);
pub const TECHS: Self = Self(Self::IDEAL_TECHS_BITS | ally_bits!(Garrus, Jacob, Mordin, Thane));
pub const BIOTICS: Self = Self(Self::IDEAL_BIOTICS_BITS | ally_bits!(Jacob, Miranda, Thane));
pub const ESCORTS: Self = Self(Self::EVERYONE_BITS & !ally_bits!(Miranda));
pub const IMMORTAL_LEADERS: Self = Self(ally_bits!(Miranda));
pub fn new<T: IntoIterator<Item = Ally>>(allies: T) -> Self {
allies.into_iter().fold(Self::NOBODY, BitOr::bitor)
}
pub const fn contains(self, ally: Ally) -> bool {
self.0 & ally.to_bit_mask() != 0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn len(self) -> usize {
self.0.count_ones() as usize
}
pub fn to_string_with_conjunction(&self, conjunction: &str) -> String {
let mut ally_names = self.into_iter().map(Ally::name);
match self.len() {
0 => "nobody".to_string(),
1 => ally_names.next().unwrap().to_string(),
2 => {
let first = ally_names.next().unwrap();
let second = ally_names.next().unwrap();
format!("{first} {conjunction} {second}")
}
len => {
if len == Self::EVERYONE.len() {
"everyone".to_string()
} else if len == Self::EVERYONE.len() - 1 {
let excluded_name = (!*self).into_iter().next().unwrap().name();
format!("everyone except {excluded_name}")
} else {
let ally_names = ally_names.map(str::to_string).collect::<Vec<_>>();
let comma_separated_list = ally_names[..len - 1].join(", ");
let last = ally_names.last().unwrap();
format!("{comma_separated_list}, {conjunction} {last}")
}
}
}
}
}
impl Debug for AllySet {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_tuple("AllySet")
.field(&format_args!("{:#06x}", self.0))
.finish()
}
}
impl Display for AllySet {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_string_with_conjunction("and"))
}
}
impl Extend<Ally> for AllySet {
fn extend<T: IntoIterator<Item = Ally>>(&mut self, allies: T) {
*self |= Self::new(allies);
}
}
impl FromIterator<Ally> for AllySet {
fn from_iter<T: IntoIterator<Item = Ally>>(allies: T) -> Self {
Self::new(allies)
}
}
impl IntoIterator for AllySet {
type IntoIter = IntoIter;
type Item = Ally;
fn into_iter(self) -> IntoIter {
IntoIter::new(self)
}
}
impl PartialOrd for AllySet {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
let common = *self & *other;
let ordering = self.len().cmp(&other.len());
match ordering {
Ordering::Equal | Ordering::Less => (*self == common).then_some(ordering),
Ordering::Greater => (*other == common).then_some(ordering),
}
}
}
impl From<Ally> for AllySet {
fn from(ally: Ally) -> Self {
Self(ally.to_bit_mask())
}
}
impl From<Option<Ally>> for AllySet {
fn from(optional_ally: Option<Ally>) -> Self {
Self(optional_ally.map_or(0, Ally::to_bit_mask))
}
}
impl<T: Into<AllySet>> BitAnd<T> for AllySet {
type Output = Self;
fn bitand(self, rhs: T) -> Self {
Self(self.0 & rhs.into().0)
}
}
impl<T: Into<AllySet>> BitAndAssign<T> for AllySet {
fn bitand_assign(&mut self, rhs: T) {
self.0 &= rhs.into().0;
}
}
impl<T: Into<AllySet>> BitOr<T> for Ally {
type Output = AllySet;
fn bitor(self, rhs: T) -> AllySet {
AllySet(self.to_bit_mask() | rhs.into().0)
}
}
impl<T: Into<AllySet>> BitOr<T> for AllySet {
type Output = Self;
fn bitor(self, rhs: T) -> Self {
Self(self.0 | rhs.into().0)
}
}
impl<T: Into<AllySet>> BitOrAssign<T> for AllySet {
fn bitor_assign(&mut self, rhs: T) {
self.0 |= rhs.into().0;
}
}
impl<T: Into<AllySet>> BitXor<T> for AllySet {
type Output = Self;
fn bitxor(self, rhs: T) -> Self {
Self(self.0 ^ rhs.into().0)
}
}
impl<T: Into<AllySet>> BitXorAssign<T> for AllySet {
fn bitxor_assign(&mut self, rhs: T) {
self.0 ^= rhs.into().0;
}
}
impl Not for Ally {
type Output = AllySet;
fn not(self) -> AllySet {
AllySet(AllySet::EVERYONE_BITS & !self.to_bit_mask())
}
}
impl Not for AllySet {
type Output = Self;
fn not(self) -> Self {
Self(Self::EVERYONE_BITS & !self.0)
}
}
impl<T: Into<AllySet>> Sub<T> for AllySet {
type Output = Self;
fn sub(self, rhs: T) -> Self {
Self(self.0 & !rhs.into().0)
}
}
impl<T: Into<AllySet>> SubAssign<T> for AllySet {
fn sub_assign(&mut self, rhs: T) {
self.0 &= !rhs.into().0;
}
}
static ALLY_ORDER: &[Ally] = &[
Ally::Garrus,
Ally::Grunt,
Ally::Jack,
Ally::Jacob,
Ally::Kasumi,
Ally::Legion,
Ally::Miranda,
Ally::Mordin,
Ally::Morinth,
Ally::Samara,
Ally::Tali,
Ally::Thane,
Ally::Zaeed,
];
#[derive(Clone)]
#[must_use]
pub struct IntoIter {
allies: AllySet,
order: std::slice::Iter<'static, Ally>,
}
impl IntoIter {
fn new(allies: AllySet) -> Self {
Self {
allies,
order: ALLY_ORDER.iter(),
}
}
}
impl Iterator for IntoIter {
type Item = Ally;
fn next(&mut self) -> Option<Ally> {
let ally = self
.order
.by_ref()
.copied()
.find(|&ally| self.allies.contains(ally));
self.allies -= ally;
ally
}
fn size_hint(&self) -> (usize, Option<usize>) {
let size = self.allies.len();
(size, Some(size))
}
}
impl DoubleEndedIterator for IntoIter {
fn next_back(&mut self) -> Option<Ally> {
let ally = self
.order
.by_ref()
.copied()
.rfind(|&ally| self.allies.contains(ally));
self.allies -= ally;
ally
}
}
impl ExactSizeIterator for IntoIter {}
impl FusedIterator for IntoIter {}
#[cfg(test)]
mod tests {
use super::*;
use Ally::*;
#[test]
fn elementwise_operations() {
let allies = Jacob | Miranda;
assert!(!allies.is_empty());
assert_eq!(allies.len(), 2);
assert!(allies.contains(Jacob));
assert!(allies.contains(Miranda));
assert!(!allies.contains(Kasumi));
let allies = (allies | Thane) - Jacob;
let mut iter = allies.into_iter();
assert_eq!(iter.next().unwrap(), Miranda);
assert_eq!(iter.next().unwrap(), Thane);
assert_eq!(iter.next(), None);
assert_eq!(allies.to_string(), "Miranda and Thane");
let allies = Mordin | Zaeed | Samara | Legion;
assert_eq!(
allies.to_string_with_conjunction("or"),
"Legion, Mordin, Samara, or Zaeed"
);
}
#[test]
fn to_string_edge_cases() {
assert_eq!(AllySet::NOBODY.to_string(), "nobody");
assert_eq!(AllySet::EVERYONE.to_string(), "everyone");
assert_eq!(AllySet::from(Morinth).to_string(), "Morinth");
}
#[test]
fn intersection() {
let a = Garrus | Grunt | Zaeed;
let b = Tali | Garrus;
assert_eq!(a & b, Garrus.into());
let c = Legion | Jack | Mordin | Samara;
assert_eq!(a & c, AllySet::NOBODY);
}
#[test]
fn difference() {
let a = Jacob | Thane | Morinth | Grunt;
let b = Morinth | Jacob | Miranda;
assert_eq!(a - b, Thane | Grunt);
}
#[test]
fn complement() {
assert_eq!(!AllySet::NOBODY, AllySet::EVERYONE);
assert_eq!(
!(Garrus | Jack | Kasumi | Miranda | Morinth | Tali | Zaeed),
Grunt | Jacob | Legion | Mordin | Samara | Thane
);
}
#[test]
fn symmetric_difference() {
let a = Legion | Thane | Jack | Morinth;
let b = Thane | Garrus | Zaeed;
assert_eq!(a ^ b, Garrus | Jack | Legion | Morinth | Zaeed);
}
#[test]
fn iter() {
let mut iter = (Tali | Grunt | Kasumi | Samara | Jacob).into_iter();
assert_eq!(iter.next(), Some(Grunt));
assert_eq!(iter.next_back(), Some(Tali));
assert_eq!(iter.next(), Some(Jacob));
assert_eq!(iter.next_back(), Some(Samara));
assert_eq!(iter.next(), Some(Kasumi));
assert_eq!(iter.next_back(), None);
assert_eq!(iter.next(), None);
}
}