use rand::Rng;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Symbol {
Skull,
Diamond,
Seven,
Lightning,
Fire,
Bar,
Cherry,
Wild,
}
impl Symbol {
pub fn icon(&self) -> &'static str {
match self {
Symbol::Skull => "💀",
Symbol::Diamond => "◆",
Symbol::Seven => "7",
Symbol::Lightning => "⚡",
Symbol::Fire => "🔥",
Symbol::Bar => "≡",
Symbol::Cherry => "●",
Symbol::Wild => "★",
}
}
pub fn label(&self) -> &'static str {
match self {
Symbol::Skull => "SKULL",
Symbol::Diamond => " GEM ",
Symbol::Seven => " 7 ",
Symbol::Lightning => "BOLT ",
Symbol::Fire => "FIRE ",
Symbol::Bar => " BAR ",
Symbol::Cherry => "CHRRY",
Symbol::Wild => "WILD ",
}
}
fn weight(&self) -> u32 {
match self {
Symbol::Skull => 2, Symbol::Diamond => 4,
Symbol::Seven => 6,
Symbol::Lightning => 8,
Symbol::Fire => 10,
Symbol::Bar => 12,
Symbol::Cherry => 14,
Symbol::Wild => 3, }
}
}
const ALL_SYMBOLS: [Symbol; 8] = [
Symbol::Skull, Symbol::Diamond, Symbol::Seven, Symbol::Lightning,
Symbol::Fire, Symbol::Bar, Symbol::Cherry, Symbol::Wild,
];
pub fn calculate_multiplier(reels: &[Symbol; 3]) -> f64 {
let [a, b, c] = reels;
let matches_with_wild = |x: &Symbol, y: &Symbol| -> bool {
*x == *y || *x == Symbol::Wild || *y == Symbol::Wild
};
let all_match = matches_with_wild(a, b) && matches_with_wild(b, c) && matches_with_wild(a, c);
if all_match {
let real = if *a != Symbol::Wild { a }
else if *b != Symbol::Wild { b }
else if *c != Symbol::Wild { c }
else { &Symbol::Wild };
return match real {
Symbol::Skull => 50.0,
Symbol::Diamond => 25.0,
Symbol::Seven => 15.0,
Symbol::Lightning => 10.0,
Symbol::Fire => 5.0,
Symbol::Bar => 3.0,
Symbol::Cherry => 2.0,
Symbol::Wild => 50.0, };
}
let two_match = matches_with_wild(a, b) || matches_with_wild(b, c) || matches_with_wild(a, c);
if two_match {
return 0.5; }
0.0 }
pub fn is_jackpot(reels: &[Symbol; 3]) -> bool {
calculate_multiplier(reels) >= 50.0
}
fn random_symbol() -> Symbol {
let total_weight: u32 = ALL_SYMBOLS.iter().map(|s| s.weight()).sum();
let mut rng = rand::rng();
let mut roll = rng.random_range(0..total_weight);
for sym in &ALL_SYMBOLS {
let w = sym.weight();
if roll < w {
return *sym;
}
roll -= w;
}
Symbol::Cherry }
pub fn generate_reel_strip(len: usize) -> Vec<Symbol> {
(0..len).map(|_| random_symbol()).collect()
}
#[derive(Debug, Clone, PartialEq)]
pub enum SlotsPhase {
Betting,
Spinning, Revealing(u8), Result,
}
pub struct SlotsGame {
pub phase: SlotsPhase,
pub bet: u64,
pub bet_input: String,
pub reels: [Symbol; 3],
pub reel_strips: [Vec<Symbol>; 3],
pub reel_pos: [usize; 3],
pub phase_timer: u64,
pub reel_speed: [u64; 3],
pub multiplier: f64,
pub last_payout: i64,
}
impl SlotsGame {
pub fn new() -> Self {
Self {
phase: SlotsPhase::Betting,
bet: 0,
bet_input: String::new(),
reels: [Symbol::Cherry; 3],
reel_strips: [
generate_reel_strip(40),
generate_reel_strip(40),
generate_reel_strip(40),
],
reel_pos: [0; 3],
phase_timer: 0,
reel_speed: [2, 2, 2], multiplier: 0.0,
last_payout: 0,
}
}
pub fn spin(&mut self, bet: u64) {
self.bet = bet;
self.last_payout = 0;
self.phase_timer = 0;
self.reels = [random_symbol(), random_symbol(), random_symbol()];
for i in 0..3 {
let mut strip = generate_reel_strip(30 + i * 8); strip.push(self.reels[i]);
self.reel_strips[i] = strip;
}
self.reel_pos = [0; 3];
self.reel_speed = [2, 2, 2];
self.phase = SlotsPhase::Spinning;
}
pub fn tick_spin(&mut self) -> bool {
self.phase_timer += 1;
match self.phase {
SlotsPhase::Spinning => {
for i in 0..3 {
if self.phase_timer % self.reel_speed[i] == 0 {
self.reel_pos[i] += 1;
}
}
if self.phase_timer > 40 {
self.phase = SlotsPhase::Revealing(0);
self.phase_timer = 0;
}
false
}
SlotsPhase::Revealing(stopped) => {
let current = stopped as usize;
for i in 0..3 {
if i <= current {
if i == current {
self.reel_speed[i] = 2 + (self.phase_timer / 8).min(6);
if self.phase_timer % self.reel_speed[i] == 0 {
self.reel_pos[i] += 1;
}
if self.reel_pos[i] >= self.reel_strips[i].len() - 1 {
self.reel_pos[i] = self.reel_strips[i].len() - 1;
if stopped < 2 {
self.phase = SlotsPhase::Revealing(stopped + 1);
self.phase_timer = 0;
} else {
self.multiplier = calculate_multiplier(&self.reels);
self.last_payout = (self.bet as f64 * self.multiplier) as i64 - self.bet as i64;
self.phase = SlotsPhase::Result;
return true;
}
}
}
} else {
if self.phase_timer % 2 == 0 {
self.reel_pos[i] += 1;
if self.reel_pos[i] >= self.reel_strips[i].len() {
self.reel_pos[i] = 0; }
}
}
}
false
}
_ => true,
}
}
pub fn display_symbol(&self, reel: usize) -> Symbol {
let strip = &self.reel_strips[reel];
if strip.is_empty() { return Symbol::Cherry; }
strip[self.reel_pos[reel] % strip.len()]
}
pub fn reel_window(&self, reel: usize) -> [Symbol; 3] {
let strip = &self.reel_strips[reel];
let len = strip.len();
if len == 0 { return [Symbol::Cherry; 3]; }
let pos = self.reel_pos[reel] % len;
let above = if pos == 0 { len - 1 } else { pos - 1 };
let below = (pos + 1) % len;
[strip[above], strip[pos], strip[below]]
}
pub fn new_spin(&mut self) {
self.phase = SlotsPhase::Betting;
self.bet = 0;
self.bet_input.clear();
self.phase_timer = 0;
self.multiplier = 0.0;
}
}
pub fn payout_table() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
("SKULL ×3", "50x", "JACKPOT"),
("GEM ×3", "25x", ""),
("7 ×3", "15x", ""),
("BOLT ×3", "10x", ""),
("FIRE ×3", " 5x", ""),
("BAR ×3", " 3x", ""),
("ANY ×3", " 2x", ""),
("ANY ×2", "0.5x", ""),
("★ WILD", "SUBS", ""),
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn three_skulls_is_jackpot() {
let reels = [Symbol::Skull, Symbol::Skull, Symbol::Skull];
assert_eq!(calculate_multiplier(&reels), 50.0);
assert!(is_jackpot(&reels));
}
#[test]
fn three_sevens() {
let reels = [Symbol::Seven, Symbol::Seven, Symbol::Seven];
assert_eq!(calculate_multiplier(&reels), 15.0);
}
#[test]
fn three_bars() {
let reels = [Symbol::Bar, Symbol::Bar, Symbol::Bar];
assert_eq!(calculate_multiplier(&reels), 3.0);
}
#[test]
fn wild_substitutes_for_match() {
let reels = [Symbol::Wild, Symbol::Seven, Symbol::Seven];
assert_eq!(calculate_multiplier(&reels), 15.0);
let reels2 = [Symbol::Diamond, Symbol::Wild, Symbol::Diamond];
assert_eq!(calculate_multiplier(&reels2), 25.0);
}
#[test]
fn three_wilds_is_jackpot() {
let reels = [Symbol::Wild, Symbol::Wild, Symbol::Wild];
assert_eq!(calculate_multiplier(&reels), 50.0);
}
#[test]
fn two_matching_returns_half() {
let reels = [Symbol::Seven, Symbol::Seven, Symbol::Bar];
assert_eq!(calculate_multiplier(&reels), 0.5);
}
#[test]
fn no_match_returns_zero() {
let reels = [Symbol::Seven, Symbol::Bar, Symbol::Fire];
assert_eq!(calculate_multiplier(&reels), 0.0);
}
#[test]
fn payout_calculation_jackpot() {
let reels = [Symbol::Skull, Symbol::Skull, Symbol::Skull];
let bet = 100u64;
let mult = calculate_multiplier(&reels);
let payout = (bet as f64 * mult) as i64 - bet as i64;
assert_eq!(payout, 4900);
}
#[test]
fn payout_calculation_loss() {
let reels = [Symbol::Seven, Symbol::Bar, Symbol::Fire];
let bet = 50u64;
let mult = calculate_multiplier(&reels);
let payout = (bet as f64 * mult) as i64 - bet as i64;
assert_eq!(payout, -50);
}
#[test]
fn payout_two_match_half_back() {
let reels = [Symbol::Bar, Symbol::Bar, Symbol::Fire];
let bet = 100u64;
let mult = calculate_multiplier(&reels);
let payout = (bet as f64 * mult) as i64 - bet as i64;
assert_eq!(payout, -50); }
#[test]
fn reel_strip_generation() {
let strip = generate_reel_strip(20);
assert_eq!(strip.len(), 20);
for s in &strip {
assert!(ALL_SYMBOLS.contains(s));
}
}
}