use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::RwLock;
use crate::solver::{CaptchaType, SolveMethod};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Arm {
pub captcha_type: String,
pub solver_method: String,
pub alpha: f64,
pub beta: f64,
}
impl Arm {
pub fn new(captcha_type: String, solver_method: String) -> Self {
Self {
captcha_type,
solver_method,
alpha: 1.0,
beta: 1.0,
}
}
pub fn posterior_mean(&self) -> f64 {
self.alpha / (self.alpha + self.beta)
}
pub fn posterior_variance(&self) -> f64 {
let a = self.alpha;
let b = self.beta;
let denom = (a + b).powi(2) * (a + b + 1.0);
(a * b) / denom
}
pub fn observe(&mut self, succeeded: bool) {
if succeeded {
self.alpha += 1.0;
} else {
self.beta += 1.0;
}
}
}
pub struct Bandit {
arms: RwLock<HashMap<(String, String), Arm>>,
decay: f64,
}
impl Bandit {
pub fn new() -> Self {
Self {
arms: RwLock::new(HashMap::new()),
decay: 1.0,
}
}
pub fn with_decay(mut self, decay: f64) -> Self {
self.decay = decay.clamp(0.5, 1.0);
self
}
pub fn choose(
&self,
captcha_type: &CaptchaType,
candidates: &[SolveMethod],
) -> Option<SolveMethod> {
if candidates.is_empty() {
return None;
}
let arms = self.arms.read().unwrap_or_else(|e| e.into_inner());
let ct = render_captcha_type(captcha_type);
let mut rng = rand::thread_rng();
let mut best: Option<(f64, SolveMethod)> = None;
for cand in candidates {
let sm = render_solve_method(cand);
let arm = arms
.get(&(ct.clone(), sm.clone()))
.cloned()
.unwrap_or_else(|| Arm::new(ct.clone(), sm.clone()));
let theta = sample_beta(arm.alpha, arm.beta, &mut rng);
if best.as_ref().map(|(b, _)| theta > *b).unwrap_or(true) {
best = Some((theta, cand.clone()));
}
}
best.map(|(_, m)| m)
}
pub fn observe(
&self,
captcha_type: &CaptchaType,
solver_method: &SolveMethod,
succeeded: bool,
) {
let ct = render_captcha_type(captcha_type);
let sm = render_solve_method(solver_method);
let mut arms = self.arms.write().unwrap_or_else(|e| e.into_inner());
if self.decay < 1.0 {
for arm in arms.values_mut() {
if arm.alpha + arm.beta > 2.0 {
arm.alpha = ((arm.alpha - 1.0) * self.decay) + 1.0;
arm.beta = ((arm.beta - 1.0) * self.decay) + 1.0;
}
}
}
let arm = arms
.entry((ct.clone(), sm.clone()))
.or_insert_with(|| Arm::new(ct, sm));
arm.observe(succeeded);
}
pub fn snapshot(&self) -> Vec<Arm> {
let arms = self.arms.read().unwrap_or_else(|e| e.into_inner());
arms.values().cloned().collect()
}
pub fn save_to(&self, path: &Path) -> std::io::Result<()> {
let snap = self.snapshot();
let s = serde_json::to_string(&snap)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(path, s)
}
pub fn load_from(&self, path: &Path) -> std::io::Result<usize> {
let body = std::fs::read_to_string(path)?;
let snap: Vec<Arm> = serde_json::from_str(&body)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let count = snap.len();
let mut arms = self.arms.write().unwrap_or_else(|e| e.into_inner());
for arm in snap {
arms.insert((arm.captcha_type.clone(), arm.solver_method.clone()), arm);
}
Ok(count)
}
pub fn len(&self) -> usize {
self.arms.read().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl Default for Bandit {
fn default() -> Self {
Self::new()
}
}
fn sample_beta(alpha: f64, beta: f64, rng: &mut impl Rng) -> f64 {
let g1 = sample_gamma(alpha, rng);
let g2 = sample_gamma(beta, rng);
if g1 + g2 == 0.0 {
return 0.5;
}
g1 / (g1 + g2)
}
fn sample_gamma(shape: f64, rng: &mut impl Rng) -> f64 {
if shape < 1.0 {
let g = sample_gamma(shape + 1.0, rng);
let u: f64 = rng.gen();
return g * u.powf(1.0 / shape);
}
let d = shape - 1.0 / 3.0;
let c = 1.0 / (9.0 * d).sqrt();
loop {
let mut x;
let mut v;
loop {
x = sample_standard_normal(rng);
v = 1.0 + c * x;
if v > 0.0 {
break;
}
}
v = v * v * v;
let u: f64 = rng.gen();
if u < 1.0 - 0.0331 * (x * x * x * x) {
return d * v;
}
if u.ln() < 0.5 * x * x + d * (1.0 - v + v.ln()) {
return d * v;
}
}
}
fn sample_standard_normal(rng: &mut impl Rng) -> f64 {
let u1: f64 = rng.gen::<f64>().max(1e-12);
let u2: f64 = rng.gen();
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f64::consts::PI * u2;
r * theta.cos()
}
fn render_captcha_type(c: &CaptchaType) -> String {
match c {
CaptchaType::Custom(name) => {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(name.as_bytes());
let d = h.finalize();
format!("Custom:{:02x}{:02x}{:02x}{:02x}", d[0], d[1], d[2], d[3])
}
other => format!("{other:?}"),
}
}
fn render_solve_method(m: &SolveMethod) -> String {
format!("{m:?}")
}
#[cfg(test)]
#[path = "bandit/tests.rs"]
mod tests;