mirror of https://github.com/Paoda/blue-gacha.git
feat: reimplement priority gacha system
This commit is contained in:
parent
41b6b0ea19
commit
b7c673cdee
33
README.md
33
README.md
|
@ -8,51 +8,52 @@ Here's an implementation of Blue Archive's Gacha System. I believe it to be corr
|
|||
Here's what's needed in order to call `.roll()` and `.roll10()`.
|
||||
|
||||
```rust
|
||||
let mut file = File::open("students.json").unwrap();
|
||||
let mut json = String::new();
|
||||
file.read_to_string(&mut json).unwrap();
|
||||
let mut students_str = String::new();
|
||||
let mut students = File::open("./examples/students.json").unwrap();
|
||||
students.read_to_string(&mut students_str).unwrap();
|
||||
|
||||
let students: Vec<Student> = serde_json::from_str(&json).unwrap();
|
||||
let students: Vec<Student> = serde_json::from_str(&students_str).unwrap();
|
||||
```
|
||||
|
||||
This Repo contains `students.json` (in the examples directory) which is an Array where each object within the array contains a Student's Japanese name, English TL name and rarity.
|
||||
|
||||
```rust
|
||||
// Here we construct a hypothetical banner featuring a gacha poll of
|
||||
// everybody but Nozomi
|
||||
let banner_students: Vec<Student> = students
|
||||
.iter()
|
||||
.filter(|student| student.name != "ノゾミ")
|
||||
.map(|student| student.clone())
|
||||
.collect();
|
||||
|
||||
let hoshino = find_student(&students, "ホシノ").unwrap();
|
||||
let shiroko = find_student(&students, "シロコ").unwrap();
|
||||
let rate_up_students = vec![shiroko, hoshino];
|
||||
let hoshino = find_student(&students, "ホシノ").unwrap()
|
||||
.into_priority_student(7.0 / 2.0);
|
||||
let shiroko = find_student(&students, "シロコ").unwrap()
|
||||
.into_priority_student(7.0 / 2.0);
|
||||
|
||||
let priority_students = vec![shiroko, hoshino];
|
||||
|
||||
let gacha = GachaBuilder::new(79.0, 18.5, 2.5)
|
||||
.with_pool(banner_students)
|
||||
.with_priority(&rate_up_students, 0.7)
|
||||
.with_priority(&priority_students)
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let pickup_banner = BannerBuilder::new("ピックアップ募集")
|
||||
.with_name_translation(Language::English, "Rate-Up Registration")
|
||||
.with_sparkable_students(&rate_up_students)
|
||||
.with_sparkable_students(&priority_students)
|
||||
.with_gacha(&gacha)
|
||||
.finish()
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
This example creates the ピックアップ募集 Banner which ran from 2021-02-04 to 2021-02-11. The only student in the game who was **not** in This banner was ノゾミ, so you can see her being filtered out of the list of *all** students to create a list of students in the ピックアップ募集.
|
||||
After selecting all the students for the banner, we want to declare which units are on rate-up if there are any. In this example, Hoshino and Shiroko have unique rates.
|
||||
|
||||
After this, we want to determine which units are on rate-up if there are any. In this example, ホシノ and シロコ have increased pull rates.
|
||||
|
||||
The Rest of the code consists of instantiating the Gacha and Banner structs using their respective Builders.
|
||||
|
||||
After this:
|
||||
The `BannerBuilder` and `GachaBuilder` structs are builders that set up the actual gacha system. After a `Banner` has successfully been built using the `Banner Builder`, we can call:
|
||||
|
||||
```rust
|
||||
let student: Student = pickup_banner.roll();
|
||||
// or
|
||||
let students: [Student; 10] = pickup_banner.roll10();
|
||||
```
|
||||
can be called (when the `Recruitment` trait is in scope) to allow for accurate simulation of Blue Archive's Gacha.
|
||||
to perform gacha rolls using the configurations encoded above.
|
|
@ -4,34 +4,47 @@ use bluearch_recruitment::i18n::Language;
|
|||
use bluearch_recruitment::student::Student;
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
const THREE_STAR_RATE: f32 = 2.5;
|
||||
const TWO_STAR_RATE: f32 = 18.5;
|
||||
const ONE_STAR_RATE: f32 = 79.0;
|
||||
|
||||
const KARIN_RATE: f32 = 0.7;
|
||||
const MUTSUKI_RATE: f32 = 0.3;
|
||||
|
||||
fn main() {
|
||||
let mut file = File::open("./examples/students.json").unwrap();
|
||||
let mut json = String::new();
|
||||
file.read_to_string(&mut json).unwrap();
|
||||
// The Banner we're rolling from is a hypothetical banner which includes every unit in the game
|
||||
// (e.g. including Nozomi)
|
||||
//
|
||||
// Karin (3*) and Mutsuki (2*) will have increased rates because I like them the most.
|
||||
// Karin will have a pull-rate of 0.7%, and Mutsuki will have a pull-rate of 3.0%
|
||||
|
||||
let students: Vec<Student> = serde_json::from_str(&json).unwrap();
|
||||
let mut students_str = String::new();
|
||||
let mut students = File::open("./examples/students.json").unwrap();
|
||||
students.read_to_string(&mut students_str).unwrap();
|
||||
|
||||
// This particular banner consists of everyone BUT Nozomi.
|
||||
let banner_students: Vec<Student> = students
|
||||
.iter()
|
||||
.filter(|student| student.name != "ノゾミ")
|
||||
.map(|student| student.clone())
|
||||
.collect();
|
||||
let students: Vec<Student> = serde_json::from_str(&students_str).unwrap();
|
||||
|
||||
// Both Hoshino and Shiroko have an increased chance of being pulled.
|
||||
let hoshino = find_student(&students, "ホシノ").unwrap();
|
||||
let shiroko = find_student(&students, "シロコ").unwrap();
|
||||
let rate_up_students = vec![shiroko, hoshino];
|
||||
let karin = find_student(&students, "カリン")
|
||||
.expect("カリン is not present in ./examples/students.json")
|
||||
.into_priority_student(KARIN_RATE);
|
||||
|
||||
let gacha = GachaBuilder::new(79.0, 18.5, 2.5)
|
||||
.with_pool(banner_students)
|
||||
.with_priority(&rate_up_students, 0.7)
|
||||
let mutsuki = find_student(&students, "ムツキ")
|
||||
.expect("ムツキ is not present in ./examples/students.json")
|
||||
.into_priority_student(MUTSUKI_RATE);
|
||||
|
||||
let sparkable = vec![karin.student().clone()];
|
||||
let priority = vec![karin, mutsuki];
|
||||
|
||||
let gacha = GachaBuilder::new(ONE_STAR_RATE, TWO_STAR_RATE, THREE_STAR_RATE)
|
||||
.with_pool(students)
|
||||
.with_priority(&priority)
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
||||
let banner = BannerBuilder::new("ピックアップ募集")
|
||||
.with_name_translation(Language::English, "Rate-Up Registration")
|
||||
.with_sparkable_students(&rate_up_students)
|
||||
// I'm some N5 loser don't judge too hard pls...
|
||||
let banner = BannerBuilder::new("不運ですね。")
|
||||
.with_name_translation(Language::English, "Unlucky, right?")
|
||||
.with_sparkable_students(&sparkable)
|
||||
.with_gacha(&gacha)
|
||||
.finish()
|
||||
.unwrap();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::gacha::{Gacha, Rarity, Recruitment};
|
||||
use crate::i18n::{I18nString, Language};
|
||||
use crate::student::Student;
|
||||
use crate::student::{PriorityStudent, Student};
|
||||
use rand::distributions::{Distribution, WeightedIndex};
|
||||
use rand::Rng;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
@ -101,15 +101,15 @@ impl BannerBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StudentType {
|
||||
One = 1, // One Star
|
||||
#[derive(Debug, Clone)]
|
||||
enum StudentType<'a> {
|
||||
One, // One Star
|
||||
Two, // Two Stars
|
||||
Three, // Three Stars
|
||||
Priority, // Rate-up Student (Presumably 3*)
|
||||
Priority(&'a PriorityStudent),
|
||||
}
|
||||
|
||||
impl From<Rarity> for StudentType {
|
||||
impl<'a> From<Rarity> for StudentType<'a> {
|
||||
fn from(rarity: Rarity) -> Self {
|
||||
match rarity {
|
||||
Rarity::One => Self::One,
|
||||
|
@ -119,15 +119,23 @@ impl From<Rarity> for StudentType {
|
|||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StudentType> for Rarity {
|
||||
impl<'a> TryFrom<StudentType<'a>> for Rarity {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: StudentType) -> Result<Self, Self::Error> {
|
||||
Rarity::try_from(&value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&StudentType<'a>> for Rarity {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(value: &StudentType<'a>) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
StudentType::One => Self::One,
|
||||
StudentType::Two => Self::Two,
|
||||
StudentType::Three => Self::Three,
|
||||
StudentType::Priority => return Err("Can not convert from Priority to Rarity"),
|
||||
StudentType::Priority(_) => return Err("Can not convert from Priority to Rarity"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -141,39 +149,59 @@ pub struct Banner {
|
|||
impl Banner {
|
||||
fn get_random_student(&self) -> Student {
|
||||
let mut rng = rand::thread_rng();
|
||||
let priority_rate = self.gacha.priority.as_ref().map_or(0, |tuple| tuple.1);
|
||||
let three_star_rate = self.gacha.get_rate(Rarity::Three) - priority_rate;
|
||||
|
||||
let items: [(StudentType, usize); 4] = [
|
||||
(StudentType::One, self.gacha.get_rate(Rarity::One)),
|
||||
(StudentType::Two, self.gacha.get_rate(Rarity::Two)),
|
||||
(StudentType::Three, three_star_rate),
|
||||
(StudentType::Priority, priority_rate),
|
||||
];
|
||||
let empty_vec = Vec::new();
|
||||
let priority_students = self.gacha.priority.as_ref().unwrap_or(&empty_vec);
|
||||
let mut rates = (
|
||||
self.gacha.get_rate(Rarity::One),
|
||||
self.gacha.get_rate(Rarity::Two),
|
||||
self.gacha.get_rate(Rarity::Three),
|
||||
);
|
||||
|
||||
for priority_student in priority_students {
|
||||
match priority_student.student().rarity {
|
||||
Rarity::One => rates.0 -= priority_student.rate,
|
||||
Rarity::Two => rates.1 -= priority_student.rate,
|
||||
Rarity::Three => rates.2 -= priority_student.rate,
|
||||
};
|
||||
}
|
||||
|
||||
let mut items: Vec<(StudentType, usize)> = Vec::with_capacity(3 + priority_students.len());
|
||||
items.push((StudentType::One, rates.0));
|
||||
items.push((StudentType::Two, rates.1));
|
||||
items.push((StudentType::Three, rates.2));
|
||||
|
||||
items.extend(
|
||||
priority_students
|
||||
.iter()
|
||||
.map(|student| (StudentType::Priority(student), student.rate)),
|
||||
);
|
||||
|
||||
let dist = WeightedIndex::new(items.iter().map(|item| item.1)).unwrap();
|
||||
let students = &self.gacha.pool;
|
||||
let student_pool = &self.gacha.pool;
|
||||
|
||||
match items[dist.sample(&mut rng)] {
|
||||
(StudentType::Priority, _) => {
|
||||
let priority_students = &self.gacha.priority.as_ref().unwrap().0;
|
||||
|
||||
let index: usize = rng.gen_range(0..priority_students.len());
|
||||
priority_students[index].clone()
|
||||
}
|
||||
(rarity, _) => {
|
||||
let students: Vec<&Student> = students
|
||||
match &items[dist.sample(&mut rng)] {
|
||||
(StudentType::Priority(priority_student), _) => priority_student.student().clone(),
|
||||
(student_type, _) => {
|
||||
let students: Vec<&Student> = student_pool
|
||||
.iter()
|
||||
.filter(|student| student.rarity == rarity.try_into().unwrap())
|
||||
.filter(|student| student.rarity == student_type.try_into().unwrap())
|
||||
.filter(|student| {
|
||||
// Remove any Rate-Up Units
|
||||
// TODO: Determine whether this is the right way of implementing priority gacha
|
||||
!priority_students
|
||||
.iter()
|
||||
.any(|priority_student| student.name == priority_student.student().name)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let index: usize = rng.gen_range(0..students.len());
|
||||
let index = rng.gen_range(0..students.len());
|
||||
students[index].clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_random_student_of_rarity(&self, rarity: Rarity) -> Student {
|
||||
// NOTE: This does not actually follow the rules of any given banner. Only get_random_student() does.
|
||||
let students = &self.gacha.pool;
|
||||
let two_star_students: Vec<&Student> = students
|
||||
.iter()
|
||||
|
@ -200,11 +228,9 @@ impl Recruitment for Banner {
|
|||
|
||||
let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two);
|
||||
|
||||
if !two_star_present {
|
||||
if students[students.len() - 1].rarity != Rarity::Three {
|
||||
if !two_star_present && students[students.len() - 1].rarity != Rarity::Three {
|
||||
students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two);
|
||||
}
|
||||
}
|
||||
|
||||
students
|
||||
}
|
||||
|
|
24
src/gacha.rs
24
src/gacha.rs
|
@ -1,4 +1,4 @@
|
|||
use crate::student::Student;
|
||||
use crate::student::{PriorityStudent, Student};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use std::cmp::Ordering;
|
||||
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
|
||||
|
@ -66,7 +66,7 @@ pub trait Recruitment {
|
|||
pub struct GachaBuilder {
|
||||
rates: Option<(usize, usize, usize)>,
|
||||
pool: Option<Vec<Student>>,
|
||||
priority: Option<(Vec<Student>, usize)>,
|
||||
priority: Option<Vec<PriorityStudent>>,
|
||||
}
|
||||
|
||||
impl Default for GachaBuilder {
|
||||
|
@ -137,18 +137,18 @@ impl GachaBuilder {
|
|||
/// # Examples
|
||||
/// ```
|
||||
/// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity};
|
||||
/// # use bluearch_recruitment::student::Student;
|
||||
/// let aru = Student::new("アル", Rarity::Three);
|
||||
/// let hina = Student::new("ヒナ", Rarity::Three);
|
||||
/// let rate_up = vec![aru, hina.clone()];
|
||||
/// let priority = vec![hina];
|
||||
/// # use bluearch_recruitment::student::{Student, PriorityStudent};
|
||||
/// let aru = Student::new("アル", Rarity::Three).into_priority_student(3.5 / 2.0);
|
||||
/// let hina = Student::new("ヒナ", Rarity::Three).into_priority_student(3.5 / 2.0);
|
||||
/// let pool = vec![aru.student().clone(), hina.student().clone()];
|
||||
/// let priority = vec![aru, hina];
|
||||
/// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5)
|
||||
/// .with_pool(rate_up)
|
||||
/// .with_priority(&priority, 3.5);
|
||||
/// .with_pool(pool)
|
||||
/// .with_priority(&priority);
|
||||
/// ```
|
||||
pub fn with_priority(self, students: &[Student], total_rate: f32) -> Self {
|
||||
pub fn with_priority(self, students: &[PriorityStudent]) -> Self {
|
||||
Self {
|
||||
priority: Some((students.to_vec(), (total_rate * 10.0) as usize)),
|
||||
priority: Some(students.to_vec()),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
@ -184,7 +184,7 @@ pub struct Gacha {
|
|||
/// (1★, 2★, 3★)
|
||||
pub rates: (usize, usize, usize),
|
||||
pub pool: Vec<Student>,
|
||||
pub priority: Option<(Vec<Student>, usize)>,
|
||||
pub priority: Option<Vec<PriorityStudent>>,
|
||||
}
|
||||
|
||||
impl Gacha {
|
||||
|
|
|
@ -60,9 +60,7 @@ impl I18nString {
|
|||
///
|
||||
/// Will return None if there is no translation for the given language
|
||||
pub fn get(&self, language: Language) -> Option<String> {
|
||||
self.translations
|
||||
.get(&language)
|
||||
.map(|message| message.clone())
|
||||
self.translations.get(&language).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,4 +55,32 @@ impl Student {
|
|||
pub fn add_translation(&mut self, language: Language, name: &str) {
|
||||
self.name.update(language, name);
|
||||
}
|
||||
|
||||
pub fn into_priority_student(self, rate: f32) -> PriorityStudent {
|
||||
PriorityStudent {
|
||||
inner: self,
|
||||
rate: (rate * 10.0) as usize,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Priority Student is a student who has a pull-rate that is unique from
|
||||
/// the rest of the rest of their peers in their star rating
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PriorityStudent {
|
||||
inner: Student,
|
||||
pub rate: usize,
|
||||
}
|
||||
|
||||
impl PriorityStudent {
|
||||
pub fn new(student: Student, rate: f32) -> Self {
|
||||
Self {
|
||||
inner: student,
|
||||
rate: (rate * 10.0) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn student(&self) -> &Student {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue