Code/SlotMachine.cs
using System;
using System.Linq;
using System.Collections.Generic;
using Sandbox;
using Casino.RpSlot.Economy;
namespace Casino.RpSlot;
// Architecture single-player par défaut.
//
// Pour utilisation en multi-joueur : le framework RP intégrateur doit
// fournir un GameObject par joueur connecté contenant un composant
// implémentant IRpEconomy. La slot machine débite/crédite via cette
// interface — c'est le rôle du framework de garantir que chaque joueur
// a son propre instance avec son propre solde.
public enum SlotState { Idle, Spinning, Resolving }
public sealed class SlotMachine : Component
{
[Property] public int MinBet { get; set; } = 10;
[Property] public int MaxBet { get; set; } = 500;
[Property] public float SpinDuration { get; set; } = 3.0f;
[Property] public float CherryMultiplier { get; set; } = 1.1f;
[Property] public float LemonMultiplier { get; set; } = 2f;
[Property] public float OrangeMultiplier { get; set; } = 5f;
[Property] public float BellMultiplier { get; set; } = 12f;
[Property] public float BarMultiplier { get; set; } = 40f;
[Property] public float SevenMultiplier { get; set; } = 150f;
[Property] public float JackpotMultiplier { get; set; } = 800f;
[Property] public float TwoCherriesMultiplier { get; set; } = 0.2f;
[Property] public int EasyChance { get; set; } = 15;
[Property] public int NeutralChance { get; set; } = 55;
[Sync] public SlotState State { get; set; } = SlotState.Idle;
[Sync] public int CurrentBet { get; set; } = 10;
[Sync] public int LastPayout { get; set; } = 0;
[Sync] public NetList<int> ReelResults { get; set; } = new();
[Sync] public NetList<int> WinningLines { get; set; } = new();
[Sync] public Guid CurrentPlayerId { get; set; }
[Sync] public bool LastSpinWasJackpot { get; private set; }
private TimeUntil _spinEnds;
private static readonly int[] SymbolWeights = new[] { 30, 25, 18, 12, 8, 5, 2 };
private enum LuckMode { Easy, Neutral, Hard }
protected override void OnUpdate()
{
if ( !Networking.IsHost ) return;
if ( State == SlotState.Spinning && _spinEnds <= 0 )
{
ResolveSpin();
}
}
[Rpc.Host]
public void RequestSpin( Guid playerId, int bet )
{
if ( State != SlotState.Idle ) return;
if ( bet < MinBet || bet > MaxBet ) return;
var playerGo = Scene.Directory.FindByGuid( playerId );
if ( playerGo == null ) return;
var economy = playerGo.Components.GetAll<Component>()
.OfType<IRpEconomy>()
.FirstOrDefault();
if ( economy == null || !economy.TryDebit( bet ) ) return;
CurrentBet = bet;
CurrentPlayerId = playerId;
LastSpinWasJackpot = false;
State = SlotState.Spinning;
_spinEnds = SpinDuration;
var rng = new Random();
LuckMode mode = RollLuckMode( rng );
GenerateBiasedReelResults( rng, mode );
BroadcastSpinStarted();
}
private void ResolveSpin()
{
State = SlotState.Resolving;
WinningLines.Clear();
int totalPayout = 0;
for ( int l = 0; l < SlotPaylines.Lines.Length; l++ )
{
var line = SlotPaylines.Lines[l];
int gain = CalculatePaylineGain( line, CurrentBet );
if ( gain > 0 )
{
WinningLines.Add( l );
totalPayout += gain;
var s0 = (SlotSymbol)ReelResults[line[0]];
var s1 = (SlotSymbol)ReelResults[line[1]];
var s2 = (SlotSymbol)ReelResults[line[2]];
if ( s0 == SlotSymbol.Jackpot && s1 == SlotSymbol.Jackpot && s2 == SlotSymbol.Jackpot )
LastSpinWasJackpot = true;
}
}
LastPayout = totalPayout;
if ( totalPayout > 0 )
{
var playerGo = Scene.Directory.FindByGuid( CurrentPlayerId );
var economy = playerGo?.Components.GetAll<Component>()
.OfType<IRpEconomy>()
.FirstOrDefault();
economy?.Credit( totalPayout );
BroadcastSpinResult( totalPayout );
}
else
{
BroadcastSpinResult( 0 );
}
State = SlotState.Idle;
}
private int CalculatePaylineGain( int[] line, int bet )
{
var s0 = (SlotSymbol)ReelResults[line[0]];
var s1 = (SlotSymbol)ReelResults[line[1]];
var s2 = (SlotSymbol)ReelResults[line[2]];
if ( s0 == s1 && s1 == s2 )
return System.Math.Max( 1, (int)( bet * GetMultiplierF( s0 ) ) );
int cherryCount = 0;
if ( s0 == SlotSymbol.Cherry ) cherryCount++;
if ( s1 == SlotSymbol.Cherry ) cherryCount++;
if ( s2 == SlotSymbol.Cherry ) cherryCount++;
if ( cherryCount == 2 )
return System.Math.Max( 1, (int)( bet * TwoCherriesMultiplier ) );
return 0;
}
private float GetMultiplierF( SlotSymbol s ) => s switch
{
SlotSymbol.Cherry => CherryMultiplier,
SlotSymbol.Lemon => LemonMultiplier,
SlotSymbol.Orange => OrangeMultiplier,
SlotSymbol.Bell => BellMultiplier,
SlotSymbol.Bar => BarMultiplier,
SlotSymbol.Seven => SevenMultiplier,
SlotSymbol.Jackpot => JackpotMultiplier,
_ => 1f
};
private LuckMode RollLuckMode( Random rng )
{
int roll = rng.Next( 0, 100 );
if ( roll < EasyChance ) return LuckMode.Easy;
if ( roll < EasyChance + NeutralChance ) return LuckMode.Neutral;
return LuckMode.Hard;
}
private void GenerateBiasedReelResults( Random rng, LuckMode mode )
{
const int maxAttempts = 30;
var temp = new int[9];
for ( int attempt = 0; attempt < maxAttempts; attempt++ )
{
for ( int i = 0; i < 9; i++ )
temp[i] = PickWeightedSymbolIndex( rng );
bool keep = mode switch
{
LuckMode.Easy => HasAnyWinningPayline( temp ),
LuckMode.Hard => !HasThreeOfKindOnAnyLine( temp ),
LuckMode.Neutral => true,
_ => true
};
if ( keep ) break;
}
ReelResults.Clear();
for ( int i = 0; i < 9; i++ ) ReelResults.Add( temp[i] );
}
private bool HasThreeOfKindOnAnyLine( int[] reels )
{
foreach ( var line in SlotPaylines.Lines )
{
if ( reels[line[0]] == reels[line[1]] && reels[line[1]] == reels[line[2]] )
return true;
}
return false;
}
private bool HasAnyWinningPayline( int[] reels )
{
foreach ( var line in SlotPaylines.Lines )
{
var s0 = (SlotSymbol)reels[line[0]];
var s1 = (SlotSymbol)reels[line[1]];
var s2 = (SlotSymbol)reels[line[2]];
if ( s0 == s1 && s1 == s2 ) return true;
int cherryCount = 0;
if ( s0 == SlotSymbol.Cherry ) cherryCount++;
if ( s1 == SlotSymbol.Cherry ) cherryCount++;
if ( s2 == SlotSymbol.Cherry ) cherryCount++;
if ( cherryCount == 2 ) return true;
}
return false;
}
private int PickWeightedSymbolIndex( Random rng )
{
int total = 0;
for ( int i = 0; i < SymbolWeights.Length; i++ ) total += SymbolWeights[i];
int roll = rng.Next( 0, total );
int cumulative = 0;
for ( int i = 0; i < SymbolWeights.Length; i++ )
{
cumulative += SymbolWeights[i];
if ( roll < cumulative ) return i;
}
return SymbolWeights.Length - 1;
}
[Rpc.Broadcast]
private void BroadcastSpinStarted()
{
var fx = Components.Get<SlotMachineFx>();
fx?.OnSpinStartedLocal();
}
[Rpc.Broadcast]
private void BroadcastSpinResult( int amount )
{
var fx = Components.Get<SlotMachineFx>();
fx?.OnSpinResultLocal( amount );
}
}