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 );
    }
}