Architettura per giochi di slot machine

1

Sto costruendo un gioco di slot machine per un cliente (soldi virtuali, non giochi a soldi veri). Hanno iniziato con un gioco di template e mi hanno assunto per reskin e apportare modifiche "minori" per una versione "rapida". Sfortunatamente, non ci sono freni sul trenino.

Il modello originale era molto semplice. Contare il numero di icone consecutive a partire da sinistra e controllare il valore nella tabella dei pagamenti. Ad esempio, se il giocatore fa girare "7 7 Chery Cherry 7", questo conta come due 7, quindi verifichiamo la tabella dei pagamenti per il valore (se ce ne sono) di ottenere due 7. C'erano anche alcuni casi speciali nel codice di valutazione per gestire icone speciali come Wild, Bonus e Scatter.

Icon aveva inizialmente tre proprietà:

string name
Image image
IconType type (Normal, Wild, Bonus, or Scatter)

La tabella dei pagamenti è una tabella semplice con tre colonne

string name, int matches, int value

Una singola classe ProcessResult gestisce i risultati per tutte le macchine; determina quale combinazione è emersa e (in base al tipo di icona) cerca il valore nella tabella dei pagamenti o applica un effetto speciale se la combinazione consiste di icone bonus o di dispersione.

Tuttavia, ogni volta che mi inviano le specifiche per una nuova macchina, il client aggiunge nuove funzionalità che non sono compatibili con il codice esistente. Ora abbiamo icone che non hanno bisogno di essere consecutive, icone che non devono iniziare a sinistra, icone che possono combinarsi con altri tipi di icone, icone che modificano il comportamento di altre icone, icone che modificano il comportamento generale della macchina, ecc. ecc.

Per un po 'stavo aggiungendo nuove proprietà alla classe Icon (ad esempio compatibleWithWilds , startsFromLeft , consecutiveOnly ), ma ora ha un sacco di proprietà che non si applicano alla maggior parte delle icone e al ProcessResult la classe è piena di casi problematici per gestire tutte queste proprietà. Sta diventando non mantenibile, e penso che sia ora di un refactoring, ma non sono sicuro di come affrontarlo.

Ho identificato diversi casi speciali che sono particolarmente problematici:

  • Le icone che non hanno bisogno di essere consecutive o iniziano da sinistra aumentano notevolmente la complessità di determinare quale icona è considerata il "risultato" per lo spin
  • Le icone che possono essere abbinate ad altre icone oltre a se stesse sono difficili da rappresentare nella tabella dei pagamenti corrente. Ad esempio, immagina uno slot a 3 rulli dove tre ciliegie pagano 5 volte, tre banane ne pagano 3 volte, ma una combinazione di ciliegie e banane paga 4 volte. La mia soluzione attuale è di assegnare loro un nome secondario con una voce separata nella tabella dei pagamenti (quindi ad esempio l'icona "ciliegia" conta come "ciliegia" o "frutto" e l'icona della banana conta come "banana" o "frutto", con "ciliegia" "banana" e "frutto" tutti elencati separatamente nella tabella dei pagamenti).
  • Alcune icone speciali di solito non hanno alcun valore di retribuzione intrinseco (ad es. "Bonus", che attiva qualche tipo di minigioco o di stato macchina alternativo) e quindi non possiamo cercare quante corrispondenze sono richieste nella tabella dei pagamenti. In questo momento la macchina ha proprietà generiche come "minBonusMatches", ma ciò significa che alcune macchine hanno proprietà per i tipi di icone che in realtà non usano e non funzionerebbero se il client avesse introdotto un secondo tipo di un'icona speciale su qualche macchina (es. "Match 3 delle icone Bonus per un bonus normale o 5 delle icone Super Bonus per un super bonus")

Non sono sicuro di come progettare l'architettura in un modo che possa facilmente adattarsi a qualsiasi nuova regola pazza che il cliente pensa. Sicuramente non voglio continuare a costruire sul groviglio attuale di% nidificato% che gestisce tutti i casi limite. Sono abbastanza sicuro non voglio scrivere una classe separata "ProcessResult" per ogni macchina, perché penso che sia confusa e incoraggi probabilmente la duplicazione del codice. Ho preso in considerazione l'estensione della classe Icon con classi figlio che definiscono la propria logica, ma penso che diventerà disordinato, e sembrerebbe come caricare la responsabilità nel posto sbagliato (le icone avrebbero quindi bisogno di sapere tutto sullo stato del gioco, che sembra indietro).

Esiste un modello o un'architettura particolare che si adatta bene qui? Il meglio che ho adesso è una vaga idea di usare qualcosa come il pattern di componenti per assegnare proprietà alle icone e alimentarle attraverso una catena di responsabilità, ma ho problemi a pensare a un'implementazione concreta, come il vecchio detto va, non può vedere la foresta per i casi limite.

Il client ignora tutti i consigli, quindi qualsiasi risposta che implichi dare consigli al client in merito allo scope creep o alla gestione del progetto è inutile

    
posta user45623 06.10.2018 - 03:30
fonte

2 risposte

5

Devi estrarre la logica per determinare una vittoria nel proprio gruppo di classi. Non è più un tavolo stupido.

In primo luogo, pensa all'astrazione.

  • Viene testato un numero di icone
  • Viene assegnato un importo vincente
  • Quando vengono trovate più combinazioni vincenti, 1 deve avere la precedenza (l'importo massimo più alto)

Quindi estrailo in un'interfaccia:

public interface IWinningCombination
{
    decimal WinningAmount { get; }
    bool IsWin(Card a, Card b, Card c, Card d, Card e);
}

Quindi definisci le tue classi di implementazione:

public AllSevensWinningCombination : IWinningCombination
{
    public decimal WinningAmount => 100;

    public bool IsWin(Card a, Card b, Card c, Card d, Card e)
    {
        return a.Value == "7"
            && b.Value == "7"
            && c.Value == "7"
            && d.Value == "7"
            && e.Value == "7"
    }
}

E la tua slot machine mantiene solo un riferimento a tutte le combinazioni vincenti e scorre su di esse in ordine di quantità vincenti:

public class SlotMachine
{
    private IWinningCombination[] winningCombinations = new IWinningCombination[]
    {
        new AllSevensWinningCombination(),
        new CrazyEightsWinningCombination(),
        new AtLeastThreeSevensWinningCombination()
    }

    private IEnumerable<IWinningCombination> WinningCombinations => winningCombinations.OrderBy(w => w.WinningAmount);

    public SpinResult Spin()
    {
        SpinResult result = // ... random spin

        foreach (var winningCombination in WinningCombinations)
        {
            if (winningCombination.IsWin(result.A, result.B, result.C, result.D, result.E))
            {
                result.Win(winningCombination.WinningAmount);

                break;
            }
        }

        return result;
    }
}

Nascondendo il controllo delle carte e il calcolo degli importi vincenti dietro un'interfaccia, puoi praticamente impazzire con le regole. In realtà, questo potrebbe anche essere un semplice motore delle regole , se vuoi un'altra parola buzz ricercabile.

    
risposta data 06.10.2018 - 04:07
fonte
0

Dovrai strutturare le classi in questo modo:

  1. Classi di rilevatori: scrivi queste classi in grado di rilevare varie potenziali combinazioni vincenti e aggiungerle a un elenco di WinEvents.

  2. Classi di Transformer: queste classi saranno probabilmente specifiche per il gioco, sebbene molti bonus possano essere comuni tra diversi giochi. Lo scopo di questa classe è trasformare i WinEvents rilevati per rilevare pattern di livello superiore, scartare WinEvents ridondanti ed eseguire altre trasformazioni che non possono essere eseguite nel rilevamento iniziale

  3. Classi CalculateWinnings: queste classi esaminano i WinEvents trasformati e calcolano le vincite e altri premi che possono essere assegnati al giocatore.

Metti insieme queste classi in questo modo:

# these are parts of the game definition
detectors : List[Detector] = [...]
transformations : List[Transformer] = [...]
paytable : Dict[WinEvent.name, CalculateWinnings] = {...}

# these are the reels state
gamestate = {...}

events : List[WinEvent] = []
prizes : List[Prize] = []

for detector in detectors:
    events.extend(detector.detect(gamestate))

for transformer in transformations:
    gamestate, events = transformer.apply(gamestate, events)

# in more complex games you may need a second line of detectors and transformers, but this would be fairly rare

for event in events:
    prizes.append(paytable[event.name].calculate(gamestate, event))

Non mettere troppi comportamenti nelle classi Icon, dovrebbero essere considerati classi di valore, ed è compito del rivelatore e del trasformatore capire se devono essere trattati in modo speciale (ad es. come caratteri jolly, icone scatter) .

Alcuni esempi di rivelatori potrebbero essere:

  • WinLineDetector (wildcard_symbols = [])
  • ScatterDetector (scatter_symbols = [])
risposta data 06.10.2018 - 06:22
fonte

Leggi altre domande sui tag