Algoritmo per l'ottimizzazione di un match game con coda nota


10

Sto cercando di scrivere un risolutore in C # .NET per un gioco noto come Flowerz. Per riferimento, puoi riprodurlo su MSN, qui: http://zone.msn.com/gameplayer/gameplayer.aspx?game=flowerz . Lo sto scrivendo per divertimento, non per nessun tipo di incarico o lavoro correlato. Per questo motivo, l'unico limite è il mio computer (un core Intel i7, con 8 GB di RAM). Non ha bisogno di correre altrove, per quanto mi riguarda.

In breve, le sue regole sono così:

  • C'è una coda piena di fiori colorati. La sua lunghezza è arbitraria
    • La coda non può essere influenzata
    • La coda viene generata all'inizio del livello
  • I fiori hanno uno o due colori.
    • Se ci sono due colori, allora c'è un colore esterno e un colore interno. Nel caso di due colori, il colore esterno viene utilizzato per l'abbinamento.
    • Se c'è una corrispondenza, il colore esterno scompare e il fiore ora è un fiore a un colore con lo stesso colore del fiore interno
  • L'obiettivo del gioco è quello di creare partite di tre (o più) dello stesso colore
    • Quando un fiore di un singolo colore fa parte di una corrispondenza, viene rimosso dal campo di gioco, creando uno spazio vuoto
    • Puoi abbinare un singolo fiore di colore al colore esterno di un fiore di due colori. In questo caso, il fiore monocolore scompare, il colore esterno del fiore bicolore scompare e il colore interno rimane
  • Vinci il round quando la coda è vuota e rimane almeno uno spazio vuoto
  • Sono possibili partite a cascata. Una cascata è quando tre (o più) fiori esterni scompaiono e quando i loro colori interni formano un'altra catena di 3 (o più fiori).
  • Il campo di gioco è sempre 7x7
  • Alcuni spazi sul campo sono coperti da rocce
    • Non puoi posizionare fiori sulle rocce
  • La coda può contenere anche una vanga che puoi usare per spostare qualsiasi fiore posizionato in uno spazio non occupato
    • Devi usare la vanga, ma in realtà non devi spostare il fiore: è perfettamente legale rimetterlo da dove è arrivato
  • La coda può contenere anche una farfalla colorata. Quando usi questa farfalla su un fiore, il fiore assume il colore della farfalla
    • Applicando una farfalla a un fiore con due colori, il fiore ottiene un solo colore, cioè quello della farfalla
    • Puoi sprecare la farfalla su uno spazio vuoto o un fiore che ha già questo colore
  • Cancellare il campo non vince la partita

L'obiettivo del risolutore è semplice: trovare un modo per svuotare la coda, con il maggior numero possibile di spazi rimanenti sul campo di gioco. Fondamentalmente, l'IA gioca il gioco per me. L'output del solutore è un elenco con le mosse trovate. Non sono interessato al punteggio, ma alla sopravvivenza il più a lungo possibile, quindi sono interessato alle mosse che lasciano il maggior numero possibile di spazi aperti.

Inutile dire che lo spazio di ricerca cresce rapidamente più aumenta la coda, quindi una forza bruta è fuori discussione. La coda inizia alle 15 e cresce con 5 ogni due o tre livelli, se ricordo bene. E, naturalmente, posizionare il primo fiore su (0,0) e il secondo su (0,1) è diverso dal posizionare il primo fiore su (1,0) e il secondo fiore su (0,0), specialmente quando il campo è già popolato con fiori di un round precedente. Una decisione così semplice potrebbe fare la differenza nel farcela o no.

Le domande che ho sono le seguenti:

  • Che tipo di problema è questo? (pensa al commesso viaggiatore, allo zaino o ad altri problemi combinatori). Sapere questo potrebbe rendere il mio Google Fu un po 'meglio.
  • Che tipo di algoritmo potrebbe darmi buoni risultati, in fretta?

Riguardo a quest'ultimo: all'inizio, ho provato a scrivere il mio algoritmo euristico (in sostanza: come lo risolverei, se conoscessi la coda?), Ma questo si traduce in molti casi limite e punteggi corrispondenti che potrei perdere.

Stavo pensando di usare un algoritmo genetico (perché almeno so come usarlo ...), ma sto avendo dei problemi nel decidere una rappresentazione binaria della scheda. Quindi c'è il problema del crossover, ma che può essere risolto con un operatore di crossover ordinato o un tipo simile di operazione.

La mia ipotesi è che il risolutore debba sempre conoscere la configurazione della scheda e la coda che sta cercando di svuotare.

Conosco alcuni altri algoritmi euristici come reti neurali e sistemi di logica fuzzy, ma mi manca l'esperienza per sapere quale è il più applicabile, o se ce ne sono altri che si adattano meglio al compito da svolgere.


Una volta ho scoperto che lo spazio di ricerca di alcuni giochi complessi su cui stavo lavorando sarebbe stato di 32 GB. All'epoca (avevo un disco da 20 Mb) sarebbe stato impossibile, ma oggigiorno è quasi fattibile nella RAM per alcuni computer.
Jonathan,

I fiori con un solo colore scompaiono del tutto se abbinati? E i fiori con due colori possono abbinare il loro strato esterno al singolo colore di un fiore a un colore? Presumo così su entrambi i punti, ma questi non sono mai esplicitamente indicati nella descrizione del problema ...
Steven Stadnicki

@StevenStadnicki Grazie! Ho aggiunto queste informazioni alla domanda originale.
user849924

1
Come piccola nota, per inciso, è estremamente probabile che la versione "booleana" di questo problema (c'è un modo per posizionare i fiori in coda per lasciare la tavola completamente vuota alla fine?) È NP-completa; ha evidenti somiglianze con il problema di Clickomania ( erikdemaine.org/clickomania ) che è NP-completo, e il problema non è più difficile di NP perché data una presunta soluzione (di lunghezza polinomiale) è facile da verificare semplicemente eseguendo la simulazione. Ciò significa che il problema di ottimizzazione è probabilmente in FP ^ NP.
Steven Stadnicki,

Risposte:


9

A prima vista , questo mi sembra essere un problema di ricerca di un singolo agente . Cioè: hai un agente (il "giocatore" AI). C'è uno stato di gioco che rappresenta lo stato del tabellone e della coda di gioco e hai una funzione successiva che può generare nuovi stati da un dato stato.

C'è anche un criterio obiettivo che ti dice quando lo stato è lo stato "risolto". E un costo di percorso - il costo di avanzamento a un dato stato (sempre "1 mossa" in questo caso).

Un puzzle prototipo di questo tipo è il 15 Puzzle . E il modo tipico di risolverlo è con una ricerca informata , ad esempio la classica ricerca euristica A * e le sue varianti.


Tuttavia, c'è un problema con questo approccio a prima vista. Algoritmi come A * sono progettati per darti il ​​percorso più breve verso un obiettivo (ad esempio: il minor numero di mosse). Nel tuo caso, il numero di mosse è sempre fisso - non esiste un percorso più breve - quindi una ricerca euristica ti darà solo un percorso per un gioco completato.

Quello che vuoi è una sequenza di mosse che ti dia il miglior stato di gioco completato.

Quindi quello che devi fare è invertire un po 'il problema. Invece che il tabellone di gioco sia lo "stato", la sequenza di mosse diventa lo "stato". (Vale a dire: posizionare gli elementi nella coda nelle posizioni "D2, A5, C7, B3, A3, ...")

Questo significa che non ci interessa davvero come vengono generati quegli stati. Il consiglio stesso è secondario, necessario solo per valutare la qualità di un determinato stato.

Ciò trasforma il problema in un problema di ottimizzazione , che può essere risolto con un algoritmo di ricerca locale (che in pratica significa creare stati attorno a un determinato stato e selezionare lo stato migliore, senza preoccuparsi del percorso tra gli stati).

Il puzzle prototipo di questo tipo è l' Eight Queens Puzzle .

In questa classe di problemi, stai cercando nello spazio degli stati per trovare una buona soluzione, in cui il "buono" viene valutato da una funzione obiettiva (chiamata anche funzione di valutazione o, per algoritmi genetici, una funzione di fitness ).

Per il tuo problema, una funzione obiettivo potrebbe restituire un valore compreso tra 0 e N, per il numero di elementi nella coda che sono stati utilizzati prima di raggiungere uno stato di errore (dove N è la lunghezza della coda). E, altrimenti, un valore di N + M, dove M è il numero di spazi vuoti lasciati sul tabellone dopo che la coda è vuota. Come tale: maggiore è il valore, "obiettivamente migliore" la soluzione.

(Vale la pena notare, a questo punto, che è necessario ottimizzare la schifezza del codice che esegue il gioco - che trasforma uno stato in una tavola finita che può essere utilizzata per la funzione obiettivo.)


Per quanto riguarda gli esempi di algoritmi di ricerca locale : il modello di base è una ricerca in salita che prende un determinato stato, lo muta e si sposta verso lo stato successivo che dà un risultato migliore.

Ovviamente questo può rimanere bloccato nei massimi locali (e simili). In questa forma si chiama avida ricerca locale . Ci sono un sacco di varianti per affrontare questo e altri problemi ( Wikipedia ti ha coperto ). Alcuni dei quali (ad esempio: ricerca del raggio locale ) tengono traccia di più stati contemporaneamente.

Una variazione particolare su questo è l' algoritmo genetico ( Wikipedia ). I passaggi di base per un algoritmo genetico sono:

  1. Determina un modo per convertire uno stato in una stringa di qualche tipo. Nel tuo caso potrebbe trattarsi di una stringa di cifre della lunghezza della coda compresa tra 1 e 49 (che rappresentano tutti i possibili posizionamenti su una scheda 7x7, probabilmente memorizzati 1 byte ciascuno). (Il tuo pezzo "spade" potrebbe essere rappresentato da due successive voci di coda, per ogni fase del movimento.)
  2. Seleziona in modo casuale una popolazione riproduttiva, dando una maggiore probabilità agli stati che hanno una migliore forma fisica . La popolazione riproduttiva dovrebbe avere le stesse dimensioni della popolazione originale: è possibile scegliere più volte stati dalla popolazione originale.
  3. Associare gli stati nella popolazione riproduttiva (il primo va con il secondo, il terzo va con il quarto, ecc.)
  4. Seleziona casualmente i punti di crossover per ogni coppia (una posizione nella stringa).
  5. Crea due prole per ogni coppia scambiando la parte della stringa dopo il punto di crossover.
  6. Mutazione casuale di ciascuno degli stati della prole. Ad esempio: scegliere casualmente di modificare una posizione casuale nella stringa in un valore casuale.
  7. Ripetere il processo con la nuova popolazione fino a quando la popolazione non converge su una o più soluzioni (o dopo un determinato numero di generazioni o viene trovata una soluzione sufficientemente buona).

Una soluzione di algoritmo genetico sembra che potrebbe sembrare appropriata per il tuo problema, con qualche aggiustamento. La più grande difficoltà che vedo è che, con la rappresentazione della stringa sopra, scoprirai che cambiare le metà della coda degli stati con metà anteriori molto diverse può provocare stati "morti" (a causa di movimenti contrastanti tra le due metà, con un punteggio di fitness basso).

Forse è possibile superare questo problema. Un'idea che viene in mente sta rendendo più probabile che gli stati con metà anteriore simili diventino coppie riproduttive. Questo potrebbe essere semplice come ordinare la popolazione riproduttiva degli stati, prima di accoppiarli. Può anche aiutare a spostare gradualmente la posizione probabile del crossover, dall'inizio alla fine della stringa, con l'aumentare del numero di generazione.

Potrebbe anche essere possibile trovare una rappresentazione delle mosse all'interno di uno stato che è più resistente (forse anche del tutto immune) allo scontro con lo stato di fallimento "quadrato pieno". Forse rappresentando le mosse come coordinate relative della mossa precedente. O avendo mosse selezionare lo spazio vuoto più vicino alla posizione data.

Come per tutti i problemi di intelligenza artificiale non banali come questo, richiederà alcuni importanti aggiustamenti.

E, come ho detto prima, l'altra grande sfida è semplicemente l'ottimizzazione della funzione obiettiva. Renderlo più veloce ti consentirà di cercare una grande quantità di spazio e di cercare soluzioni per i giochi con code più lunghe.


Per questa risposta, in particolare per ottenere tutta la terminologia corretta, ho dovuto approfondire il mio libro di testo AI sull'università, "Intelligenza artificiale: un approccio moderno" di Russell e Norvig. Non sono sicuro che sia "buono" (non ho altri testi AI con cui confrontarlo), ma non è male. Almeno è abbastanza grande;)


Ho identificato quel problema anche con un crossover: è molto probabile che un bambino abbia più oggetti posizionati di quelli disponibili in coda (tipo di mancanza GA per TSP: potrebbe visitare le città due o più (o per niente!) Dopo un crossover: forse un crossover ordinato ( permutationcity.co.uk/projects/mutants/tsp.html ) potrebbe funzionare, in particolare quando si imposta la sequenza di mosse sullo stato.
imposta

Non sono sicuro che sia del tutto giusto - nella mia mente, lo stato di fallimento è che un pezzo è posto in una posizione che è già occupata (terminando così quel gioco in anticipo, con un risultato di fitness basso). Quindi la lunghezza della coda corrisponde alla lunghezza della stringa genetica - non è mai la lunghezza sbagliata. Tuttavia, potresti essere interessato a qualcosa con l'idea di scambiare e ordinare. Se un determinato ordine si traduce in un gioco completato e si scambiano due mosse, immagino che ci sia una possibilità molto migliore che lo stato mutato sia anche un gioco completo rispetto a se si dovessero semplicemente impostare una (o due?) Posizioni di mossa in modo casuale .
Andrew Russell,

Lo stato di errore è quando non hai più opzioni per posizionare le mosse, cioè quando esaurisci gli spazi vuoti e non si verificano corrispondenze con quella mossa. Simile a quello che stai dicendo: devi posizionarlo in una posizione che è già occupata (ma questo è vero solo quando non ci sono più posti per cominciare). Il crossover che ho pubblicato potrebbe essere interessante. Il cromosoma A ha elementi posizionati su A1, B1, ..., G1, A2, B2 e C2 e il cromosoma B su G7 ... A7, G6, F6 ed E6. Seleziona alcuni randoms da A e mantieni il loro indice. Seleziona il complemento A da B e mantieni il loro indice e unisci per un bambino.
user849924

"Problema" con questo crossover è che sono consentite più mosse nello stesso punto. Ma ciò dovrebbe essere facilmente risolvibile con qualcosa di simile a SimulateAutomaticChanges dalla soluzione di Stefan K: applica il set di mosse / stato del bambino allo stato di base (applica semplicemente tutte le mosse, una per una) del campo di gioco e se lo stato di accettazione (coda vuota ) non può essere raggiunto (perché devi posizionare un fiore in un punto occupato), quindi il bambino non è valido e dovremo riprodursi di nuovo. Ecco dove compare la tua condizione di errore. Lo capisco adesso, eh. : D
user849924

Lo accetto come risposta, per due motivi. Primo: mi hai dato l'idea di cui avevo bisogno per far funzionare GA per questo problema. Secondo: eri il primo. ; p
user849924

2

categorizzazione

La risposta non è facile. La teoria dei giochi ha alcune classificazioni per i giochi, ma sembra che non ci sia una corrispondenza 1: 1 chiara per quel gioco con una teoria speciale. È una forma speciale di problema combinatorio.

Non è un commesso viaggiatore, che deciderebbe per un ordine in cui visiti "nodi" con un certo costo per raggiungere il nodo successivo dall'ultimo. Non è possibile riordinare la coda, né è necessario utilizzare tutti i campi sulla mappa.

Lo zaino non corrisponde perché alcuni campi si svuotano mentre si inseriscono alcuni elementi nello "zaino". Quindi è forse una forma estesa di questo, ma molto probabilmente gli algoritmi non saranno applicabili a causa di ciò.

Wikipedia fornisce alcuni suggerimenti sulla categorizzazione qui: http://en.wikipedia.org/wiki/Game_theory#Types_of_games

Lo classificherei come "problema di controllo ottimale a tempo discreto" ( http://en.wikipedia.org/wiki/Optimal_control ), ma non credo che questo ti aiuterà.

algoritmi

Se conosci davvero la coda completa, puoi applicare gli algoritmi di ricerca dell'albero. Come hai detto, la complessità del problema aumenta molto rapidamente con la lunghezza della coda. Suggerisco di utilizzare un algoritmo come "Ricerca approfondita (DFS)", che non richiede molta memoria. Dato che il punteggio non ti interessa, potresti fermarti dopo aver trovato la prima soluzione. Per decidere quale sotto-ramo cercare prima, è necessario applicare un'euristica per l'ordinamento. Ciò significa che dovresti scrivere una funzione di valutazione (ad esempio: numero di campi vuoti; più sofisticato è, meglio è), che fornisce un punteggio per confrontare quale mossa successiva è la più promettente.

Sono quindi necessarie solo le seguenti parti:

  1. modello dello stato del gioco, che memorizza tutte le informazioni del gioco (ad es. stato / mappa del tabellone, coda, numero di mossa / posizione in coda)
  2. un generatore di mosse, che ti dà tutte le mosse valide per un dato stato di gioco
  3. una funzione "do move" e una "undo move"; che applicano / annullano una determinata mossa (valida) in uno stato di gioco. Considerando che la funzione "sposta" dovrebbe memorizzare alcune "informazioni di annullamento" per la funzione "annulla". Copiare lo stato del gioco e modificarlo in ogni iterazione rallenta significativamente la ricerca! Prova almeno a memorizzare lo stato nello stack (= variabili locali, nessuna allocazione dinamica usando "nuovo").
  4. una funzione di valutazione, che fornisce un punteggio comparabile per ogni stato del gioco
  5. funzione di ricerca

Ecco un'implementazione di riferimento incompleta per la ricerca approfondita:

public class Item
{
    // TODO... represents queue items (FLOWER, SHOVEL, BUTTERFLY)
}

public class Field
{
    // TODO... represents field on the board (EMPTY or FLOWER)
}

public class Modification {
    int x, y;
    Field originalValue, newValue;

    public Modification(int x, int y, Field originalValue, newValue) {
        this.x = x;
        this.y = y;
        this.originalValue = originalValue;
        this.newValue = newValue;
    }

    public void Do(GameState state) {
        state.board[x,y] = newValue;
    }

    public void Undo(GameState state) {
        state.board[x,y] = originalValue;
    }
}

class Move : ICompareable {

    // score; from evaluation function
    public int score; 

    // List of modifications to do/undo to execute the move or to undo it
    Modification[] modifications;

    // Information for later knowing, what "control" action has been chosen
    public int x, y;   // target field chosen
    public int x2, y2; // secondary target field chosen (e.g. if moving a field)


    public Move(GameState state, Modification[] modifications, int score, int x, int y, int x2 = -1, int y2 = -1) {
        this.modifications = modifications;
        this.score = score;
        this.x = x;
        this.y = y;
        this.x2 = x2;
        this.y2 = y2;
    }

    public int CompareTo(Move other)
    {
        return other.score - this.score; // less than 0, if "this" precededs "other"...
    }

    public virtual void Do(GameState state)
    {
        foreach(Modification m in modifications) m.Do(state);
        state.queueindex++;
    }

    public virtual void Undo(GameState state)
    {
        --state.queueindex;
        for (int i = m.length - 1; i >= 0; --i) m.Undo(state); // undo modification in reversed order
    }
}

class GameState {
    public Item[] queue;
    public Field[][] board;
    public int queueindex;

    public GameState(Field[][] board, Item[] queue) {
        this.board = board;
        this.queue = queue;
        this.queueindex = 0;
    }

    private int Evaluate()
    {
        int value = 0;
        // TODO: Calculate some reasonable value for the game state...

        return value;
    }

    private List<Modification> SimulateAutomaticChanges(ref int score) {
        List<Modification> modifications = new List<Modification>();
        // TODO: estimate all "remove" flowers or recoler them according to game rules 
        // and store all changes into modifications...
        if (modifications.Count() > 0) {
            foreach(Modification modification in modifications) modification.Do(this);

            // Recursively call this function, for cases of chain reactions...
            List<Modification> moreModifications = SimulateAutomaticChanges();

            foreach(Modification modification in modifications) modification.Undo(this);

            // Add recursively generated moves...
            modifications.AddRange(moreModifications);
        } else {
            score = Evaluate();
        }

        return modifications;
    }

    // Helper function for move generator...
    private void MoveListAdd(List<Move> movelist, List<Modifications> modifications, int x, int y, int x2 = -1, int y2 = -1) {
        foreach(Modification modification in modifications) modification.Do(this);

        int score;
        List<Modification> autoChanges = SimulateAutomaticChanges(score);

        foreach(Modification modification in modifications) modification.Undo(this);

        modifications.AddRange(autoChanges);

        movelist.Add(new Move(this, modifications, score, x, y, x2, y2));
    }


    private List<Move> getValidMoves() {
        List<Move> movelist = new List<Move>();
        Item nextItem = queue[queueindex];
        const int MAX = board.length * board[0].length + 2;

        if (nextItem.ItemType == Item.SHOVEL)
        {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: Check if valid, else "continue;"

                    for (int x2 = 0; x2 < board.length; ++x2)
                    {
                        for(int y2 = 0; y2 < board[x].length; ++y2) {
                            List<Modifications> modifications = new List<Modifications>();

                            Item fromItem = board[x][y];
                            Item toItem = board[x2][y2];
                            modifications.Add(new Modification(x, y, fromItem, Item.NONE));
                            modifications.Add(new Modification(x2, y2, toItem, fromItem));

                            MoveListAdd(movelist, modifications, x, y, x2, y2);
                        }
                    }
                }
            }

        } else {

            for (int x = 0; x < board.length; ++x)
            {
                for (int y = 0; y < board[x].length; ++y)
                {
                    // TODO: check if nextItem may be applied here... if not "continue;"

                    List<Modifications> modifications = new List<Modifications>();
                    if (nextItem.ItemType == Item.FLOWER) {
                        // TODO: generate modifications for putting flower at x,y
                    } else {
                        // TODO: generate modifications for putting butterfly "nextItem" at x,y
                    }

                    MoveListAdd(movelist, modifications, x, y);
                }
            }
        }

        // Sort movelist...
        movelist.Sort();

        return movelist;
    }


    public List<Move> Search()
    {
        List<Move> validmoves = getValidMoves();

        foreach(Move move in validmoves) {
            move.Do(this);
            List<Move> solution = Search();
            if (solution != null)
            {
                solution.Prepend(move);
                return solution;
            }
            move.Undo(this);
        }

        // return "null" as no solution was found in this branch...
        // this will also happen if validmoves == empty (e.g. lost game)
        return null;
    }
}

Questo codice non è verificato per funzionare, né è compilabile o completo. Ma dovrebbe darti un'idea di come farlo. Il lavoro più importante è la funzione di valutazione. Quanto più sofisticato è, tanto più "sbagliato" l'algoritmo proverà (e dovrà annullare) in seguito. Ciò riduce estremamente la complessità.

Se questo è troppo lento, puoi anche provare ad applicare alcuni metodi di giochi a due persone come HashTables. Per questo dovrai calcolare una chiave hash (iterativa) per ogni stato di gioco che valuti e contrassegnare gli stati che non portano a una soluzione. Ad esempio, ogni volta che il metodo Search () restituisce "null", è necessario creare una voce HashTable e quando si immette Search () si verifica se questo stato è già stato raggiunto finora senza risultati positivi e in tal caso restituisce "null" senza ulteriori indagini. Per questo avrai bisogno di un'enorme tabella hash e dovresti accettare "collisioni hash" che potrebbero causare che probabilmente non trovi una soluzione esistente, ma che è molto improbabile, se le tue funzioni hash sono abbastanza buone e la tua tabella è abbastanza grande (è un rischio di rischio calcolabile).

Penso che non ci siano altri algoritmi per risolvere questo problema (come descritto da te) più efficiente, supponendo che la tua funzione di valutazione sia ottimale ...


Sì, posso conoscere la coda completa. Un'implementazione della funzione di valutazione considererebbe anche un posizionamento valido, ma potenzialmente negativo? Potenzialmente male essere una mossa come metterla vicino al fiore di un colore diverso quando c'è già un colore simile sul campo? O posizionando un fiore da qualche parte quali blocchi di una corrispondenza totalmente diversa a causa della mancanza di spazio?
user849924

Questa risposta mi ha dato idee per il modello e come lavorare con le regole del gioco, quindi lo voterò. Grazie per il tuo contributo!
user849924

@ user849924: Sì, ovviamente la funzione di valutazione deve calcolare un "valore" di valutazione per questo. Più lo stato di gioco attuale peggiora (vicino alla perdita), peggiore dovrebbe essere il valore di valutazione restituito. La valutazione più semplice sarebbe quella di restituire il numero di campi vuoti. Puoi migliorare questo aggiungendo 0,1 per ogni fiore posizionato accanto a un fiore di colore simile. Per verificare la tua funzione scegli alcuni stati di gioco casuali, calcola il loro valore e confrontali. Se pensi che lo stato A sia migliore dello stato B, il punteggio prima di A dovrebbe essere migliore di quello per B.
SDwarfs
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.