Progettazione di un gioco a turni in cui le azioni hanno effetti collaterali


19

Sto scrivendo una versione per computer del gioco Dominion . È un gioco di carte a turni in cui le carte azione, le carte tesoro e le carte punto vittoria vengono accumulate nel mazzo personale di un giocatore. Ho una struttura di classe abbastanza ben sviluppata e sto iniziando a progettare la logica del gioco. Sto usando Python e potrei aggiungere una semplice GUI con Pygame in seguito.

La sequenza di turni dei giocatori è governata da una macchina a stati molto semplice. Il turno passa in senso orario e un giocatore non può uscire dal gioco prima che sia finito. Il gioco di un singolo turno è anche una macchina a stati; in generale, i giocatori passano attraverso una "fase di azione", una "fase di acquisto" e una "fase di pulizia" (in questo ordine). Basato sulla risposta alla domanda Come implementare il motore di gioco a turni? , la macchina a stati è una tecnica standard per questa situazione.

Il mio problema è che durante la fase di azione di un giocatore, può usare una carta azione che ha effetti collaterali, sia su se stessa, sia su uno o più degli altri giocatori. Ad esempio, una carta azione consente a un giocatore di effettuare un secondo turno immediatamente dopo la conclusione del turno corrente. Un'altra carta azione fa sì che tutti gli altri giocatori scartino due carte dalle loro mani. Ancora un'altra carta azione non fa nulla per il turno corrente, ma consente a un giocatore di pescare carte extra nel suo turno successivo. Per rendere le cose ancora più complicate, ci sono spesso nuove espansioni nel gioco che aggiungono nuove carte. Mi sembra che codificare con esattezza i risultati di ogni carta azione nella macchina a stati del gioco sarebbe sia brutto che inadattabile. La risposta al ciclo strategico a turni non entra in un livello di dettaglio che affronta i progetti per risolvere questo problema.

Che tipo di modello di programmazione dovrei usare per comprendere il fatto che il modello generale per i turni può essere modificato da azioni che si svolgono all'interno del turno? L'oggetto di gioco dovrebbe tenere traccia degli effetti di ogni carta azione? Oppure, se le carte dovessero implementare i propri effetti (ad esempio implementando un'interfaccia), quale configurazione è richiesta per dare loro abbastanza energia? Ho escogitato alcune soluzioni a questo problema, ma mi chiedo se esiste un modo standard per risolverlo. In particolare, mi piacerebbe sapere quale oggetto / classe / qualunque cosa sia responsabile di tenere traccia delle azioni che ogni giocatore deve fare in conseguenza di una carta azione giocata, e anche come ciò si riferisce a cambiamenti temporanei nella normale sequenza di la macchina a virata.


2
Ciao Apis Utilis e benvenuti in GDSE. La tua domanda è ben scritta ed è bello che tu abbia fatto riferimento alle domande relative. Tuttavia, la tua domanda copre molti problemi diversi e, per coprirla completamente, una domanda dovrebbe probabilmente essere enorme. Potresti comunque ottenere una buona risposta, ma te stesso e il sito trarranno vantaggio se risolvi ulteriormente il problema. Forse iniziare con la costruzione di un gioco più semplice e costruire fino a Dominion?
michael.bartnett,

1
Comincerei dal dare a ogni carta uno script che modifica lo stato del gioco e, se non succede nulla di strano,
ripiego le

Risposte:


11

Concordo con Jari Komppa che definire gli effetti delle carte con un potente linguaggio di scripting è la strada da percorrere. Ma credo che la chiave per la massima flessibilità sia la gestione degli eventi tramite script.

Al fine di consentire alle carte di interagire con eventi di gioco successivi, è possibile aggiungere un'API di scripting per aggiungere "hook di script" a determinati eventi, come l'inizio e la fine delle fasi di gioco o determinate azioni che i giocatori possono eseguire. Ciò significa che lo script che viene eseguito quando una carta viene giocata è in grado di registrare una funzione che viene chiamata al successivo raggiungimento di una fase specifica. Il numero di funzioni che possono essere registrate per ciascun evento dovrebbe essere illimitato. Quando ce n'è più di uno, vengono quindi chiamati nel loro ordine di registrazione (a meno che, naturalmente, non vi sia una regola di gioco principale che dice qualcosa di diverso).

Dovrebbe essere possibile registrare questi hook per tutti i giocatori o solo per alcuni giocatori. Suggerirei anche di aggiungere la possibilità per gli hook di decidere autonomamente se devono continuare a essere chiamati o meno. In questi esempi il valore restituito della funzione hook (true o false) viene utilizzato per esprimere questo.

La tua carta a doppio turno farebbe quindi qualcosa del genere:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Non ho idea se Dominion abbia anche qualcosa di simile a una "fase di pulizia" - in questo esempio è l'ipotetica ultima fase del turno dei giocatori)

Una carta che consente a ogni giocatore di pescare una carta aggiuntiva all'inizio della sua fase di pesca sarebbe simile a questa:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Una carta che fa perdere al giocatore bersaglio un punto bersaglio ogni volta che gioca una carta sarebbe simile a questa:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

Non dovrai aggirare il codice di alcune azioni di gioco come pescare carte o perdere punti ferita, perché la loro definizione completa - che cosa significa esattamente "pescare una carta" - fa parte delle meccaniche di gioco principali. Ad esempio, conosco alcuni TCG in cui quando devi pescare una carta per qualsiasi motivo e il tuo mazzo è vuoto, perdi la partita. Questa regola non è stampata su ogni carta che ti fa pescare carte, perché è nel libro delle regole. Quindi non dovresti controllare nemmeno quella condizione di perdita nello script di ogni carta. Controllare cose del genere dovrebbe far parte della drawCard()funzione hard-coded (che, a proposito, sarebbe anche un buon candidato per un evento hookable).

A proposito: è improbabile che sarai in grado di pianificare in anticipo per ogni oscuro meccanico che potrebbero venire le future edizioni , quindi qualunque cosa tu faccia, dovrai comunque aggiungere nuove funzionalità per le future edizioni di tanto in tanto (in questo caso, un minigioco di lancio di coriandoli).


1
Wow. Quella confusione dei confetti.
Jari Komppa,

Risposta eccellente, @Philipp, e questo si occupa di molte cose fatte in Dominion. Tuttavia, ci sono azioni che devono avvenire immediatamente quando una carta viene giocata, ovvero una carta che costringe un altro giocatore a girare la prima carta del suo grimorio e consente al giocatore attuale di dire "Conservalo" o "Scarta". Scriveresti hook di eventi per occuparti di tali azioni immediate o avresti bisogno di trovare metodi aggiuntivi per scrivere le carte?
febbraio

2
Quando qualcosa deve accadere immediatamente, lo script deve chiamare direttamente le funzioni appropriate e non registrare una funzione hook.
Philipp

@JariKomppa: il set Unglued era volutamente privo di senso e pieno di carte pazze che non avevano senso. La mia preferita era una carta che faceva infliggere a tutti un punto di danno quando dicevano una parola particolare. Ho scelto "il".
Jack Aidley,

9

Ho dato questo problema - motore di gioco di carte computerizzato flessibile - alcuni hanno pensato qualche tempo fa.

Prima di tutto, un gioco di carte complesso come Chez Geek o Fluxx (e, credo, Dominion) richiederebbe che le carte siano programmabili. Fondamentalmente ogni carta verrebbe con il suo mazzo di script che potrebbe cambiare lo stato del gioco in vari modi. Questo ti permetterebbe di dare al sistema un po 'di prova per il futuro, poiché gli script potrebbero essere in grado di fare cose a cui non riesci a pensare in questo momento, ma potrebbero venire in una futura espansione.

In secondo luogo, la "svolta" rigida potrebbe causare problemi.

Hai bisogno di una sorta di "pila di turni" che contenga i "turni speciali", come "scarta 2 carte". Quando la pila è vuota, il turno normale predefinito continua.

In Fluxx, è del tutto possibile che un turno vada in qualcosa del tipo:

  • Scegli N carte (come indicato dalle regole attuali, modificabili tramite carte)
  • Gioca a N carte (come indicato dalle regole attuali, modificabili tramite carte)
    • Una delle carte può essere "prendi 3, gioca 2"
      • Una di quelle carte potrebbe essere "fare un altro turno"
    • Una delle carte può essere "scarta e pesca"
  • Se cambi regole per scegliere più carte di quelle che hai fatto all'inizio del tuo turno, scegli più carte
  • Se cambi le regole per un minor numero di carte in mano, tutti gli altri devono scartare immediatamente le carte
  • Quando il tuo turno termina, scarta le carte fino a quando non hai N carte (modificabili tramite carte, di nuovo), quindi fai un altro turno (se hai giocato una carta "fai un altro turno" qualche volta nel pasticcio sopra).

..E così via e così via. Quindi progettare una struttura a turni in grado di gestire l'abuso sopra può essere piuttosto complicato. Aggiungete a ciò i numerosi giochi con carte "quando" (come in "chez geek") in cui le carte "quando" possono interrompere il normale flusso, ad esempio, annullando l'ultima carta giocata.

Quindi, fondamentalmente, inizierei dal progettare una struttura di turni molto flessibile, progettandola in modo che possa essere descritta come una sceneggiatura (poiché ogni gioco avrebbe bisogno del proprio "master script" che gestisca la struttura di base del gioco). Quindi, qualsiasi carta dovrebbe essere scrivibile; la maggior parte delle carte probabilmente non fa nulla di strano, ma altre lo fanno. Le carte possono anche avere vari attributi: se possono essere tenute in mano, giocate "ogni volta che", se possono essere conservate come risorse (come "custodi" di fluxx o varie cose in "chez geek" come cibo) ...

In realtà non ho mai iniziato a implementare nulla di tutto ciò, quindi in pratica potresti trovare molte altre sfide. Il modo più semplice per iniziare sarebbe iniziare con qualsiasi cosa tu sappia del sistema che vuoi implementare e implementarli in modo gestibile da script, posizionando il meno possibile nella pietra, quindi quando si verifica un'espansione, non dovrai rivedere il sistema di base - molto. =)


Questa è un'ottima risposta, e avrei accettato entrambi se avessi potuto. Ho rotto il pareggio accettando la risposta della persona con la reputazione inferiore :)
Apis Utilis

Nessun problema, ormai ci sono abituato. =)
Jari Komppa,

0

Hearthstone sembra fare cose relative e onestamente penso che il modo migliore per raggiungere la flessibilità sia attraverso un motore ECS con un design orientato ai dati. Ho cercato di creare un clone di Hearthstone e diversamente è stato dimostrato impossibile. Tutti i casi limite. Se stai affrontando molti di questi strani casi limite, questo è probabilmente il modo migliore per farlo. Sono piuttosto di parte per esperienza recente provando questa tecnica.

Modifica: ECS potrebbe non essere nemmeno necessario a seconda del tipo di flessibilità e ottimizzazione che desideri. È solo un modo per raggiungere questo obiettivo. DOD Ho erroneamente considerato una programmazione procedurale sebbene si riferiscano molto. Ciò che voglio dire è. Che dovresti considerare di eliminare OOP del tutto o almeno almeno e focalizzare invece la tua attenzione sui dati e su come sono organizzati. Evitare eredità e metodi. Concentrati invece su funzioni pubbliche (sistemi) per manipolare i dati della tua carta. Ogni azione non è qualcosa di logico o logico di alcun tipo, ma è invece dati grezzi. Dove quindi i tuoi sistemi lo usano per eseguire la logica. L'intero caso dell'interruttore o l'utilizzo di un numero intero per accedere a una matrice di puntatori a funzione aiuta a capire in modo efficiente la logica desiderata dai dati di input.

Le regole di base da seguire sono che dovresti evitare di legare la logica direttamente con i dati, dovresti evitare che i dati dipendano il più possibile l'uno dall'altro (potrebbero esserci delle eccezioni) e che quando desideri una logica flessibile che ti senta fuori portata ... Valuta di convertirlo in dati.

Ci sono dei vantaggi nel farlo. Ogni carta può avere un valore enum (s) o stringhe (s) per rappresentare le loro azioni. Questo stagista ti consente di progettare carte attraverso file di testo o json e consentire al programma di importarli automaticamente. Se trasformi le azioni dei giocatori in un elenco di dati, ciò offre una flessibilità ancora maggiore, specialmente se una carta dipende dalla logica passata come fa hearthstone, o se desideri salvare il gioco o ripetere una partita in qualsiasi momento. C'è il potenziale per creare l'IA più facilmente. Soprattutto quando si utilizza un "sistema di utilità" anziché un "albero dei comportamenti". Anche il networking diventa più facile perché invece di dover capire come ottenere interi oggetti polimorfici da trasferire sul filo e come verrebbe impostata la serializzazione dopo il fatto, il fatto che i tuoi oggetti di gioco siano già nient'altro che semplici dati che finiscono per essere veramente facili da spostare. E ultimo ma sicuramente non meno importante, questo ti consente di ottimizzare più facilmente perché, invece di perdere tempo a preoccuparti del codice, sei in grado di organizzare meglio i tuoi dati in modo che il processore abbia un tempo più facile a gestirli. Python può avere problemi qui, ma cerca "cache line" e come si collega allo sviluppatore del gioco. Non è importante per la prototipazione di roba forse, ma lungo la strada tornerà utile alla grande.

Alcuni link utili.

Nota: ECS consente di aggiungere / rimuovere dinamicamente variabili (chiamate componenti) in fase di esecuzione. Un esempio di programma su come "ECS" potrebbe apparire (ci sono molti modi per farlo).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Crea un gruppo di entità con dati di trama e posizione e alla fine distrugge un'entità che ha un componente di trama che si trova al terzo indice dell'array di componenti di trama. Sembra eccentrico ma è un modo di fare le cose. Ecco un esempio di come renderizzeresti tutto ciò che ha un componente texture.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}

1
Questa risposta sarebbe migliore se entrasse in qualche dettaglio in più su come consiglieresti di impostare il tuo ECS orientato ai dati e applicarlo per risolvere questo specifico problema.
DMGregory

Aggiornato grazie per averlo sottolineato.
Blue_Pyro

In generale, penso che sia male dire a qualcuno "come" impostare questo tipo di approccio, ma invece lasciare che progettino la propria soluzione. Dimostra di essere sia un buon modo di praticare che consente una soluzione potenzialmente migliore al problema. Quando si pensa ai dati più che alla logica in questo modo, finisce per essere che ci sono molti modi per realizzare la stessa cosa e tutto dipende dalle esigenze dell'applicazione. Oltre al tempo / conoscenza del programmatore.
Blue_Pyro
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.