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()`.
|
Here's what's needed in order to call `.roll()` and `.roll10()`.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let mut file = File::open("students.json").unwrap();
|
let mut students_str = String::new();
|
||||||
let mut json = String::new();
|
let mut students = File::open("./examples/students.json").unwrap();
|
||||||
file.read_to_string(&mut 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.
|
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
|
```rust
|
||||||
|
// Here we construct a hypothetical banner featuring a gacha poll of
|
||||||
|
// everybody but Nozomi
|
||||||
let banner_students: Vec<Student> = students
|
let banner_students: Vec<Student> = students
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|student| student.name != "ノゾミ")
|
.filter(|student| student.name != "ノゾミ")
|
||||||
.map(|student| student.clone())
|
.map(|student| student.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let hoshino = find_student(&students, "ホシノ").unwrap();
|
let hoshino = find_student(&students, "ホシノ").unwrap()
|
||||||
let shiroko = find_student(&students, "シロコ").unwrap();
|
.into_priority_student(7.0 / 2.0);
|
||||||
let rate_up_students = vec![shiroko, hoshino];
|
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)
|
let gacha = GachaBuilder::new(79.0, 18.5, 2.5)
|
||||||
.with_pool(banner_students)
|
.with_pool(banner_students)
|
||||||
.with_priority(&rate_up_students, 0.7)
|
.with_priority(&priority_students)
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let pickup_banner = BannerBuilder::new("ピックアップ募集")
|
let pickup_banner = BannerBuilder::new("ピックアップ募集")
|
||||||
.with_name_translation(Language::English, "Rate-Up Registration")
|
.with_name_translation(Language::English, "Rate-Up Registration")
|
||||||
.with_sparkable_students(&rate_up_students)
|
.with_sparkable_students(&priority_students)
|
||||||
.with_gacha(&gacha)
|
.with_gacha(&gacha)
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap();
|
.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 `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:
|
||||||
|
|
||||||
The Rest of the code consists of instantiating the Gacha and Banner structs using their respective Builders.
|
|
||||||
|
|
||||||
After this:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let student: Student = pickup_banner.roll();
|
let student: Student = pickup_banner.roll();
|
||||||
// or
|
// or
|
||||||
let students: [Student; 10] = pickup_banner.roll10();
|
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 bluearch_recruitment::student::Student;
|
||||||
use std::{fs::File, io::Read};
|
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() {
|
fn main() {
|
||||||
let mut file = File::open("./examples/students.json").unwrap();
|
// The Banner we're rolling from is a hypothetical banner which includes every unit in the game
|
||||||
let mut json = String::new();
|
// (e.g. including Nozomi)
|
||||||
file.read_to_string(&mut json).unwrap();
|
//
|
||||||
|
// 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 students: Vec<Student> = serde_json::from_str(&students_str).unwrap();
|
||||||
let banner_students: Vec<Student> = students
|
|
||||||
.iter()
|
|
||||||
.filter(|student| student.name != "ノゾミ")
|
|
||||||
.map(|student| student.clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Both Hoshino and Shiroko have an increased chance of being pulled.
|
let karin = find_student(&students, "カリン")
|
||||||
let hoshino = find_student(&students, "ホシノ").unwrap();
|
.expect("カリン is not present in ./examples/students.json")
|
||||||
let shiroko = find_student(&students, "シロコ").unwrap();
|
.into_priority_student(KARIN_RATE);
|
||||||
let rate_up_students = vec![shiroko, hoshino];
|
|
||||||
|
|
||||||
let gacha = GachaBuilder::new(79.0, 18.5, 2.5)
|
let mutsuki = find_student(&students, "ムツキ")
|
||||||
.with_pool(banner_students)
|
.expect("ムツキ is not present in ./examples/students.json")
|
||||||
.with_priority(&rate_up_students, 0.7)
|
.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()
|
.finish()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let banner = BannerBuilder::new("ピックアップ募集")
|
// I'm some N5 loser don't judge too hard pls...
|
||||||
.with_name_translation(Language::English, "Rate-Up Registration")
|
let banner = BannerBuilder::new("不運ですね。")
|
||||||
.with_sparkable_students(&rate_up_students)
|
.with_name_translation(Language::English, "Unlucky, right?")
|
||||||
|
.with_sparkable_students(&sparkable)
|
||||||
.with_gacha(&gacha)
|
.with_gacha(&gacha)
|
||||||
.finish()
|
.finish()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::gacha::{Gacha, Rarity, Recruitment};
|
use crate::gacha::{Gacha, Rarity, Recruitment};
|
||||||
use crate::i18n::{I18nString, Language};
|
use crate::i18n::{I18nString, Language};
|
||||||
use crate::student::Student;
|
use crate::student::{PriorityStudent, Student};
|
||||||
use rand::distributions::{Distribution, WeightedIndex};
|
use rand::distributions::{Distribution, WeightedIndex};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use std::convert::{TryFrom, TryInto};
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
@ -101,15 +101,15 @@ impl BannerBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
enum StudentType {
|
enum StudentType<'a> {
|
||||||
One = 1, // One Star
|
One, // One Star
|
||||||
Two, // Two Stars
|
Two, // Two Stars
|
||||||
Three, // Three 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 {
|
fn from(rarity: Rarity) -> Self {
|
||||||
match rarity {
|
match rarity {
|
||||||
Rarity::One => Self::One,
|
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;
|
type Error = &'static str;
|
||||||
|
|
||||||
fn try_from(value: StudentType) -> Result<Self, Self::Error> {
|
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 {
|
Ok(match value {
|
||||||
StudentType::One => Self::One,
|
StudentType::One => Self::One,
|
||||||
StudentType::Two => Self::Two,
|
StudentType::Two => Self::Two,
|
||||||
StudentType::Three => Self::Three,
|
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 {
|
impl Banner {
|
||||||
fn get_random_student(&self) -> Student {
|
fn get_random_student(&self) -> Student {
|
||||||
let mut rng = rand::thread_rng();
|
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] = [
|
let empty_vec = Vec::new();
|
||||||
(StudentType::One, self.gacha.get_rate(Rarity::One)),
|
let priority_students = self.gacha.priority.as_ref().unwrap_or(&empty_vec);
|
||||||
(StudentType::Two, self.gacha.get_rate(Rarity::Two)),
|
let mut rates = (
|
||||||
(StudentType::Three, three_star_rate),
|
self.gacha.get_rate(Rarity::One),
|
||||||
(StudentType::Priority, priority_rate),
|
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 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)] {
|
match &items[dist.sample(&mut rng)] {
|
||||||
(StudentType::Priority, _) => {
|
(StudentType::Priority(priority_student), _) => priority_student.student().clone(),
|
||||||
let priority_students = &self.gacha.priority.as_ref().unwrap().0;
|
(student_type, _) => {
|
||||||
|
let students: Vec<&Student> = student_pool
|
||||||
let index: usize = rng.gen_range(0..priority_students.len());
|
|
||||||
priority_students[index].clone()
|
|
||||||
}
|
|
||||||
(rarity, _) => {
|
|
||||||
let students: Vec<&Student> = students
|
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
|
let index = rng.gen_range(0..students.len());
|
||||||
let index: usize = rng.gen_range(0..students.len());
|
|
||||||
students[index].clone()
|
students[index].clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_random_student_of_rarity(&self, rarity: Rarity) -> Student {
|
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 students = &self.gacha.pool;
|
||||||
let two_star_students: Vec<&Student> = students
|
let two_star_students: Vec<&Student> = students
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -200,10 +228,8 @@ impl Recruitment for Banner {
|
||||||
|
|
||||||
let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two);
|
let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two);
|
||||||
|
|
||||||
if !two_star_present {
|
if !two_star_present && students[students.len() - 1].rarity != Rarity::Three {
|
||||||
if students[students.len() - 1].rarity != Rarity::Three {
|
students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two);
|
||||||
students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
students
|
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 serde_repr::{Deserialize_repr, Serialize_repr};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
|
||||||
|
@ -66,7 +66,7 @@ pub trait Recruitment {
|
||||||
pub struct GachaBuilder {
|
pub struct GachaBuilder {
|
||||||
rates: Option<(usize, usize, usize)>,
|
rates: Option<(usize, usize, usize)>,
|
||||||
pool: Option<Vec<Student>>,
|
pool: Option<Vec<Student>>,
|
||||||
priority: Option<(Vec<Student>, usize)>,
|
priority: Option<Vec<PriorityStudent>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GachaBuilder {
|
impl Default for GachaBuilder {
|
||||||
|
@ -137,18 +137,18 @@ impl GachaBuilder {
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
/// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity};
|
/// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity};
|
||||||
/// # use bluearch_recruitment::student::Student;
|
/// # use bluearch_recruitment::student::{Student, PriorityStudent};
|
||||||
/// let aru = Student::new("アル", Rarity::Three);
|
/// let aru = Student::new("アル", Rarity::Three).into_priority_student(3.5 / 2.0);
|
||||||
/// let hina = Student::new("ヒナ", Rarity::Three);
|
/// let hina = Student::new("ヒナ", Rarity::Three).into_priority_student(3.5 / 2.0);
|
||||||
/// let rate_up = vec![aru, hina.clone()];
|
/// let pool = vec![aru.student().clone(), hina.student().clone()];
|
||||||
/// let priority = vec![hina];
|
/// let priority = vec![aru, hina];
|
||||||
/// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5)
|
/// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5)
|
||||||
/// .with_pool(rate_up)
|
/// .with_pool(pool)
|
||||||
/// .with_priority(&priority, 3.5);
|
/// .with_priority(&priority);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_priority(self, students: &[Student], total_rate: f32) -> Self {
|
pub fn with_priority(self, students: &[PriorityStudent]) -> Self {
|
||||||
Self {
|
Self {
|
||||||
priority: Some((students.to_vec(), (total_rate * 10.0) as usize)),
|
priority: Some(students.to_vec()),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ pub struct Gacha {
|
||||||
/// (1★, 2★, 3★)
|
/// (1★, 2★, 3★)
|
||||||
pub rates: (usize, usize, usize),
|
pub rates: (usize, usize, usize),
|
||||||
pub pool: Vec<Student>,
|
pub pool: Vec<Student>,
|
||||||
pub priority: Option<(Vec<Student>, usize)>,
|
pub priority: Option<Vec<PriorityStudent>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Gacha {
|
impl Gacha {
|
||||||
|
|
|
@ -60,9 +60,7 @@ impl I18nString {
|
||||||
///
|
///
|
||||||
/// Will return None if there is no translation for the given language
|
/// Will return None if there is no translation for the given language
|
||||||
pub fn get(&self, language: Language) -> Option<String> {
|
pub fn get(&self, language: Language) -> Option<String> {
|
||||||
self.translations
|
self.translations.get(&language).cloned()
|
||||||
.get(&language)
|
|
||||||
.map(|message| message.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,4 +55,32 @@ impl Student {
|
||||||
pub fn add_translation(&mut self, language: Language, name: &str) {
|
pub fn add_translation(&mut self, language: Language, name: &str) {
|
||||||
self.name.update(language, name);
|
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