Melhor pontuação Boggle Board


16

Eu estava interessado em ver as respostas para essa pergunta (agora extinta) , mas ela nunca foi corrigida / aprimorada.

Dado um conjunto de dados Boggle de 6 lados (configuração roubada desta pergunta ), determine em dois minutos de tempo de processamento qual configuração do cartão permitirá a maior pontuação possível. (ou seja, quais dados em qual local com qual lado para cima permite o maior pool de palavras com pontuação?)


OBJETIVO

  • Seu código deve ser executado por não mais de 2 minutos (120 segundos). Nesse momento, ele deve parar automaticamente de executar e imprimir os resultados.

  • A pontuação final do desafio será a pontuação média do Boggle de 5 execuções do programa.

    • Em caso de empate, o vencedor será o algoritmo que encontrou mais palavras.
    • Caso ainda haja um empate, o vencedor será o algoritmo que encontrar mais palavras longas (8 ou mais) .

REGRAS / RESTRIÇÕES

  • Este é um desafio de código; o comprimento do código é irrelevante.

  • Consulte este link para obter uma lista de palavras (lista de uso ISPELL "english.0"- a lista SCOWL está faltando algumas palavras bastante comuns).

    • Esta listagem pode ser consultada / importada / lida no seu código da maneira que você desejar.
    • Somente palavras correspondidas pelo regex ^([a-pr-z]|qu){3,16}$serão contadas. (Somente letras minúsculas, de 3 a 16 caracteres, qu devem ser usadas como uma unidade.)
  • As palavras são formadas vinculando letras adjacentes (horizontal, vertical e diagonal) para soletrar as palavras na ordem correta, sem usar um único dado mais de uma vez em uma única palavra.

    • As palavras devem ter 3 letras ou mais; palavras mais curtas não ganharão pontos.
    • Cartas duplicadas são aceitáveis, mas não dados.
    • Palavras que ultrapassam as bordas / cruzam de um lado para o outro não são permitidas.
  • A pontuação final do Boggle ( sem desafio ) é o total dos valores em pontos para todas as palavras encontradas.

    • O valor do ponto atribuído a cada palavra é baseado no comprimento da palavra. (ver abaixo)
    • As regras normais do Boggle deduziriam / descontariam palavras encontradas por outro jogador. Suponha que nenhum outro jogador esteja envolvido e todas as palavras encontradas contam para a pontuação total.
    • No entanto, palavras encontradas mais de uma vez na mesma grade devem ser contadas apenas uma vez.
  • Sua função / programa deve ENCONTRAR o arranjo ideal; simplesmente codificar uma lista predeterminada não funciona.

  • Sua saída deve ser uma grade 4x4 do seu tabuleiro ideal, uma lista de todas as palavras encontradas para esse tabuleiro e a pontuação do Boggle para corresponder a essas palavras.


MORRER CONFIGURAÇÃO

A  A  E  E  G  N
E  L  R  T  T  Y
A  O  O  T  T  W
A  B  B  J  O  O
E  H  R  T  V  W
C  I  M  O  T  U
D  I  S  T  T  Y
E  I  O  S  S  T
D  E  L  R  V  Y
A  C  H  O  P  S
H  I  M  N  Qu U
E  E  I  N  S  U
E  E  G  H  N  W
A  F  F  K  P  S
H  L  N  N  R  Z
D  E  I  L  R  X

MESA DE PONTUAÇÃO PADRÃO

Word length => Points
<= 2 - 0 pts
   3 - 1  
   4 - 1  
   5 - 2  
   6 - 3  
   7 - 5
>= 8 - 11 pts
*Words using the "Qu" die will count the full 2 letters for their word, not just the 1 die.

EXEMPLO DE SAÍDA

A  L  O  J  
V  U  T  S  
L  C  H  E  
G  K  R  X

CUT
THE
LUCK
HEX
....

140 points

Se precisar de mais esclarecimentos, pergunte!


2
Eu preferiria ter um dicionário fornecido para padronizar o objetivo. Observe também que essa não é uma idéia nova, pois uma simples pesquisa no Google revelará :) A pontuação mais alta que já vi é 4527( 1414total de palavras), encontrada aqui: ai.stanford.edu/~chuongdo/boggle/index.html
mellamokb 26/04/12

4
O programa é necessário para terminar este século?
Peter Taylor

11
@GlitchMr Em inglês, Q normalmente é usado apenas com U. Boggle, colocando as duas letras no mesmo dado que uma unidade.
Gaffi

11
A especificação da lista de palavras não é clara. Você está contando apenas as palavras listadas em inglês.0 em minúsculas? (As regras padrão do jogo de palavras excluem abreviações / inicialismos e substantivos próprios).
Peter Taylor

11
Eu estava pensando em regex ^([a-pr-z]|qu){3,16}$(que excluiria incorretamente palavras de três letras com qu, mas não há nenhuma).
Peter Taylor

Respostas:


9

C, com média de 500+ 1500 1750 pontos

Este é um aprimoramento relativamente menor em relação à versão 2 (veja abaixo as notas nas versões anteriores). Existem duas partes. Primeiro: em vez de selecionar pranchas aleatoriamente da piscina, o programa agora repete todas as pranchas da piscina, usando cada uma delas antes de retornar ao topo da piscina e repetir. (Como o pool está sendo modificado enquanto essa iteração ocorre, ainda haverá placas escolhidas duas vezes seguidas, ou pior, mas isso não é uma preocupação séria.) A segunda alteração é que o programa agora rastreia quando o pool muda e se o programa demorar muito sem melhorar o conteúdo do pool, ele determina que a pesquisa "parou", esvazia o pool e inicia novamente com uma nova pesquisa. Ele continua fazendo isso até que os dois minutos terminem.

Inicialmente, pensei que estaria empregando algum tipo de pesquisa heurística para ir além da faixa de 1500 pontos. O comentário de @ mellamokb sobre um quadro de 4527 pontos me levou a supor que havia muito espaço para melhorias. No entanto, estamos usando uma lista de palavras relativamente pequena. O quadro de 4527 pontos estava usando o YAWL, que é a lista de palavras mais inclusiva do mercado - é ainda maior que a lista de palavras oficial do Scrabble dos EUA. Com isso em mente, examinei novamente as placas que meu programa havia encontrado e notei que parecia haver um conjunto limitado de placas acima de 1700. Por exemplo, eu tive várias execuções que descobriram uma placa com a pontuação de 1726, mas sempre foi exatamente a mesma placa que foi encontrada (ignorando rotações e reflexões).

Como outro teste, executei meu programa usando YAWL como dicionário e ele encontrou o quadro de 4527 pontos após cerca de uma dúzia de execuções. Diante disso, suponho que meu programa já esteja no limite superior do espaço de pesquisa e, portanto, a reescrita que eu estava planejando introduziria complexidade extra com muito pouco ganho.

Aqui está minha lista dos cinco quadros com maior pontuação que meu programa encontrou usando a lista de english.0palavras:

1735 :  D C L P  E I A E  R N T R  S E G S
1738 :  B E L S  R A D G  T I N E  S E R S
1747 :  D C L P  E I A E  N T R D  G S E R
1766 :  M P L S  S A I E  N T R N  D E S G
1772:   G R E P  T N A L  E S I T  D R E S

Minha crença é que o "quadro grep" de 1772 (como passei a chamá-lo), com 531 palavras, é o quadro com a maior pontuação possível com esta lista de palavras. Mais de 50% das execuções de dois minutos do meu programa terminam com este quadro. Também deixei meu programa em execução durante a noite sem encontrar nada melhor. Portanto, se houver um quadro de pontuação mais alta, é provável que ele tenha algum aspecto que derrote a técnica de pesquisa do programa. Um quadro em que todas as pequenas alterações possíveis no layout causam uma enorme queda na pontuação total, por exemplo, nunca podem ser descobertas pelo meu programa. Meu palpite é que é improvável que exista tal placa.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#define WORDLISTFILE "./english.0"

#define XSIZE 4
#define YSIZE 4
#define BOARDSIZE (XSIZE * YSIZE)
#define DIEFACES 6
#define WORDBUFSIZE 256
#define MAXPOOLSIZE 32
#define STALLPOINT 64
#define RUNTIME 120

/* Generate a random int from 0 to N-1.
 */
#define random(N)  ((int)(((double)(N) * rand()) / (RAND_MAX + 1.0)))

static char const dice[BOARDSIZE][DIEFACES] = {
    "aaeegn", "elrtty", "aoottw", "abbjoo",
    "ehrtvw", "cimotu", "distty", "eiosst",
    "delrvy", "achops", "himnqu", "eeinsu",
    "eeghnw", "affkps", "hlnnrz", "deilrx"
};

/* The dictionary is represented in memory as a tree. The tree is
 * represented by its arcs; the nodes are implicit. All of the arcs
 * emanating from a single node are stored as a linked list in
 * alphabetical order.
 */
typedef struct {
    int letter:8;   /* the letter this arc is labelled with */
    int arc:24;     /* the node this arc points to (i.e. its first arc) */
    int next:24;    /* the next sibling arc emanating from this node */
    int final:1;    /* true if this arc is the end of a valid word */
} treearc;

/* Each of the slots that make up the playing board is represented
 * by the die it contains.
 */
typedef struct {
    unsigned char die;      /* which die is in this slot */
    unsigned char face;     /* which face of the die is showing */
} slot;

/* The following information defines a game.
 */
typedef struct {
    slot board[BOARDSIZE];  /* the contents of the board */
    int score;              /* how many points the board is worth */
} game;

/* The wordlist is stored as a binary search tree.
 */
typedef struct {
    int item: 24;   /* the identifier of a word in the list */
    int left: 16;   /* the branch with smaller identifiers */
    int right: 16;  /* the branch with larger identifiers */
} listnode;

/* The dictionary.
 */
static treearc *dictionary;
static int heapalloc;
static int heapsize;

/* Every slot's immediate neighbors.
 */
static int neighbors[BOARDSIZE][9];

/* The wordlist, used while scoring a board.
 */
static listnode *wordlist;
static int listalloc;
static int listsize;
static int xcursor;

/* The game that is currently being examined.
 */
static game G;

/* The highest-scoring game seen so far.
 */
static game bestgame;

/* Variables to time the program and display stats.
 */
static time_t start;
static int boardcount;
static int allscores;

/* The pool contains the N highest-scoring games seen so far.
 */
static game pool[MAXPOOLSIZE];
static int poolsize;
static int cutoffscore;
static int stallcounter;

/* Some buffers shared by recursive functions.
 */
static char wordbuf[WORDBUFSIZE];
static char gridbuf[BOARDSIZE];

/*
 * The dictionary is stored as a tree. It is created during
 * initialization and remains unmodified afterwards. When moving
 * through the tree, the program tracks the arc that points to the
 * current node. (The first arc in the heap is a dummy that points to
 * the root node, which otherwise would have no arc.)
 */

static void initdictionary(void)
{
    heapalloc = 256;
    dictionary = malloc(256 * sizeof *dictionary);
    heapsize = 1;
    dictionary->arc = 0;
    dictionary->letter = 0;
    dictionary->next = 0;
    dictionary->final = 0;
}

static int addarc(int arc, char ch)
{
    int prev, a;

    prev = arc;
    a = dictionary[arc].arc;
    for (;;) {
        if (dictionary[a].letter == ch)
            return a;
        if (!dictionary[a].letter || dictionary[a].letter > ch)
            break;
        prev = a;
        a = dictionary[a].next;
    }
    if (heapsize >= heapalloc) {
        heapalloc *= 2;
        dictionary = realloc(dictionary, heapalloc * sizeof *dictionary);
    }
    a = heapsize++;
    dictionary[a].letter = ch;
    dictionary[a].final = 0;
    dictionary[a].arc = 0;
    if (prev == arc) {
        dictionary[a].next = dictionary[prev].arc;
        dictionary[prev].arc = a;
    } else {
        dictionary[a].next = dictionary[prev].next;
        dictionary[prev].next = a;
    }
    return a;
}

static int validateword(char *word)
{
    int i;

    for (i = 0 ; word[i] != '\0' && word[i] != '\n' ; ++i)
        if (word[i] < 'a' || word[i] > 'z')
            return 0;
    if (word[i] == '\n')
        word[i] = '\0';
    if (i < 3)
        return 0;
    for ( ; *word ; ++word, --i) {
        if (*word == 'q') {
            if (word[1] != 'u')
                return 0;
            memmove(word + 1, word + 2, --i);
        }
    }
    return 1;
}

static void createdictionary(char const *filename)
{
    FILE *fp;
    int arc, i;

    initdictionary();
    fp = fopen(filename, "r");
    while (fgets(wordbuf, sizeof wordbuf, fp)) {
        if (!validateword(wordbuf))
            continue;
        arc = 0;
        for (i = 0 ; wordbuf[i] ; ++i)
            arc = addarc(arc, wordbuf[i]);
        dictionary[arc].final = 1;
    }
    fclose(fp);
}

/*
 * The wordlist is stored as a binary search tree. It is only added
 * to, searched, and erased. Instead of storing the actual word, it
 * only retains the word's final arc in the dictionary. Thus, the
 * dictionary needs to be walked in order to print out the wordlist.
 */

static void initwordlist(void)
{
    listalloc = 16;
    wordlist = malloc(listalloc * sizeof *wordlist);
    listsize = 0;
}

static int iswordinlist(int word)
{
    int node, n;

    n = 0;
    for (;;) {
        node = n;
        if (wordlist[node].item == word)
            return 1;
        if (wordlist[node].item > word)
            n = wordlist[node].left;
        else
            n = wordlist[node].right;
        if (!n)
            return 0;
    }
}

static int insertword(int word)
{
    int node, n;

    if (!listsize) {
        wordlist->item = word;
        wordlist->left = 0;
        wordlist->right = 0;
        ++listsize;
        return 1;
    }

    n = 0;
    for (;;) {
        node = n;
        if (wordlist[node].item == word)
            return 0;
        if (wordlist[node].item > word)
            n = wordlist[node].left;
        else
            n = wordlist[node].right;
        if (!n)
            break;
    }

    if (listsize >= listalloc) {
        listalloc *= 2;
        wordlist = realloc(wordlist, listalloc * sizeof *wordlist);
    }
    n = listsize++;
    wordlist[n].item = word;
    wordlist[n].left = 0;
    wordlist[n].right = 0;
    if (wordlist[node].item > word)
        wordlist[node].left = n;
    else
        wordlist[node].right = n;
    return 1;
}

static void clearwordlist(void)
{
    listsize = 0;
    G.score = 0;
}


static void scoreword(char const *word)
{
    int const scoring[] = { 0, 0, 0, 1, 1, 2, 3, 5 };
    int n, u;

    for (n = u = 0 ; word[n] ; ++n)
        if (word[n] == 'q')
            ++u;
    n += u;
    G.score += n > 7 ? 11 : scoring[n];
}

static void addwordtolist(char const *word, int id)
{
    if (insertword(id))
        scoreword(word);
}

static void _printwords(int arc, int len)
{
    int a;

    while (arc) {
        a = len + 1;
        wordbuf[len] = dictionary[arc].letter;
        if (wordbuf[len] == 'q')
            wordbuf[a++] = 'u';
        if (dictionary[arc].final) {
            if (iswordinlist(arc)) {
                wordbuf[a] = '\0';
                if (xcursor == 4) {
                    printf("%s\n", wordbuf);
                    xcursor = 0;
                } else {
                    printf("%-16s", wordbuf);
                    ++xcursor;
                }
            }
        }
        _printwords(dictionary[arc].arc, a);
        arc = dictionary[arc].next;
    }
}

static void printwordlist(void)
{
    xcursor = 0;
    _printwords(1, 0);
    if (xcursor)
        putchar('\n');
}

/*
 * The board is stored as an array of oriented dice. To score a game,
 * the program looks at each slot on the board in turn, and tries to
 * find a path along the dictionary tree that matches the letters on
 * adjacent dice.
 */

static void initneighbors(void)
{
    int i, j, n;

    for (i = 0 ; i < BOARDSIZE ; ++i) {
        n = 0;
        for (j = 0 ; j < BOARDSIZE ; ++j)
            if (i != j && abs(i / XSIZE - j / XSIZE) <= 1
                       && abs(i % XSIZE - j % XSIZE) <= 1)
                neighbors[i][n++] = j;
        neighbors[i][n] = -1;
    }
}

static void printboard(void)
{
    int i;

    for (i = 0 ; i < BOARDSIZE ; ++i) {
        printf(" %c", toupper(dice[G.board[i].die][G.board[i].face]));
        if (i % XSIZE == XSIZE - 1)
            putchar('\n');
    }
}

static void _findwords(int pos, int arc, int len)
{
    int ch, i, p;

    for (;;) {
        ch = dictionary[arc].letter;
        if (ch == gridbuf[pos])
            break;
        if (ch > gridbuf[pos] || !dictionary[arc].next)
            return;
        arc = dictionary[arc].next;
    }
    wordbuf[len++] = ch;
    if (dictionary[arc].final) {
        wordbuf[len] = '\0';
        addwordtolist(wordbuf, arc);
    }
    gridbuf[pos] = '.';
    for (i = 0 ; (p = neighbors[pos][i]) >= 0 ; ++i)
        if (gridbuf[p] != '.')
            _findwords(p, dictionary[arc].arc, len);
    gridbuf[pos] = ch;
}

static void findwordsingrid(void)
{
    int i;

    clearwordlist();
    for (i = 0 ; i < BOARDSIZE ; ++i)
        gridbuf[i] = dice[G.board[i].die][G.board[i].face];
    for (i = 0 ; i < BOARDSIZE ; ++i)
        _findwords(i, 1, 0);
}

static void shuffleboard(void)
{
    int die[BOARDSIZE];
    int i, n;

    for (i = 0 ; i < BOARDSIZE ; ++i)
        die[i] = i;
    for (i = BOARDSIZE ; i-- ; ) {
        n = random(i);
        G.board[i].die = die[n];
        G.board[i].face = random(DIEFACES);
        die[n] = die[i];
    }
}

/*
 * The pool contains the N highest-scoring games found so far. (This
 * would typically be done using a priority queue, but it represents
 * far too little of the runtime. Brute force is just as good and
 * simpler.) Note that the pool will only ever contain one board with
 * a particular score: This is a cheap way to discourage the pool from
 * filling up with almost-identical high-scoring boards.
 */

static void addgametopool(void)
{
    int i;

    if (G.score < cutoffscore)
        return;
    for (i = 0 ; i < poolsize ; ++i) {
        if (G.score == pool[i].score) {
            pool[i] = G;
            return;
        }
        if (G.score > pool[i].score)
            break;
    }
    if (poolsize < MAXPOOLSIZE)
        ++poolsize;
    if (i < poolsize) {
        memmove(pool + i + 1, pool + i, (poolsize - i - 1) * sizeof *pool);
        pool[i] = G;
    }
    cutoffscore = pool[poolsize - 1].score;
    stallcounter = 0;
}

static void selectpoolmember(int n)
{
    G = pool[n];
}

static void emptypool(void)
{
    poolsize = 0;
    cutoffscore = 0;
    stallcounter = 0;
}

/*
 * The program examines as many boards as it can in the given time,
 * and retains the one with the highest score. If the program is out
 * of time, then it reports the best-seen game and immediately exits.
 */

static void report(void)
{
    findwordsingrid();
    printboard();
    printwordlist();
    printf("score = %d\n", G.score);
    fprintf(stderr, "// score: %d points (%d words)\n", G.score, listsize);
    fprintf(stderr, "// %d boards examined\n", boardcount);
    fprintf(stderr, "// avg score: %.1f\n", (double)allscores / boardcount);
    fprintf(stderr, "// runtime: %ld s\n", time(0) - start);
}

static void scoreboard(void)
{
    findwordsingrid();
    ++boardcount;
    allscores += G.score;
    addgametopool();
    if (bestgame.score < G.score) {
        bestgame = G;
        fprintf(stderr, "// %ld s: board %d scoring %d\n",
                time(0) - start, boardcount, G.score);
    }

    if (time(0) - start >= RUNTIME) {
        G = bestgame;
        report();
        exit(0);
    }
}

static void restartpool(void)
{
    emptypool();
    while (poolsize < MAXPOOLSIZE) {
        shuffleboard();
        scoreboard();
    }
}

/*
 * Making small modifications to a board.
 */

static void turndie(void)
{
    int i, j;

    i = random(BOARDSIZE);
    j = random(DIEFACES - 1) + 1;
    G.board[i].face = (G.board[i].face + j) % DIEFACES;
}

static void swapdice(void)
{
    slot t;
    int p, q;

    p = random(BOARDSIZE);
    q = random(BOARDSIZE - 1);
    if (q >= p)
        ++q;
    t = G.board[p];
    G.board[p] = G.board[q];
    G.board[q] = t;
}

/*
 *
 */

int main(void)
{
    int i;

    start = time(0);
    srand((unsigned int)start);

    createdictionary(WORDLISTFILE);
    initwordlist();
    initneighbors();

    restartpool();
    for (;;) {
        for (i = 0 ; i < poolsize ; ++i) {
            selectpoolmember(i);
            turndie();
            scoreboard();
            selectpoolmember(i);
            swapdice();
            scoreboard();
        }
        ++stallcounter;
        if (stallcounter >= STALLPOINT) {
            fprintf(stderr, "// stalled; restarting search\n");
            restartpool();
        }
    }

    return 0;
}

Notas para a versão 2 (9 de junho)

Aqui está uma maneira de usar a versão inicial do meu código como um ponto de partida. As alterações nesta versão consistem em menos de 100 linhas, mas triplicaram a pontuação média do jogo.

Nesta versão, o programa mantém um "pool" de candidatos, composto pelos N quadros de maior pontuação que o programa gerou até o momento. Toda vez que um novo quadro é gerado, ele é adicionado ao pool e o quadro de menor pontuação do pool é removido (que pode muito bem ser o quadro que acabou de ser adicionado, se sua pontuação for menor do que o que já existe). O pool é preenchido inicialmente com painéis gerados aleatoriamente, após o qual mantém um tamanho constante durante toda a execução do programa.

O loop principal do programa consiste em selecionar um tabuleiro aleatório do pool e alterá-lo, determinar a pontuação desse novo tabuleiro e colocá-lo no pool (se tiver uma pontuação suficiente). Dessa maneira, o programa aprimora continuamente as placas de alta pontuação. A principal atividade é fazer melhorias incrementais passo a passo, mas o tamanho do pool também permite que o programa encontre aprimoramentos em várias etapas que pioram temporariamente a pontuação de um conselho antes que ele possa melhorar.

Normalmente, este programa encontra um bom máximo local rapidamente, após o que, presumivelmente, qualquer máximo melhor está muito distante para ser encontrado. E, mais uma vez, não faz sentido executar o programa por mais de 10 segundos. Isso pode ser melhorado, por exemplo, com o programa detectando essa situação e iniciando uma nova pesquisa com um novo pool de candidatos. No entanto, isso representaria apenas um aumento marginal. Uma técnica de pesquisa heurística adequada provavelmente seria uma avenida melhor de exploração.

(Nota: vi que esta versão estava gerando cerca de 5k placas / s. Como a primeira versão normalmente produzia 20k placas / s, fiquei inicialmente preocupado. Após a criação de perfis, no entanto, descobri que o tempo extra era gasto gerenciando a lista de palavras. Em outras palavras, isso se deveu inteiramente ao fato de o programa ter encontrado muito mais palavras por painel.Em vista disso, considerei mudar o código para gerenciar a lista de palavras, mas, como esse programa está usando apenas 10 dos 120 segundos alocados, uma otimização seria muito prematura.)

Notas para a versão 1 (2 de junho)

Esta é uma solução muito, muito simples. Tudo isso gera placas aleatórias e, depois de 10 segundos, produz aquela com a maior pontuação. (O padrão foi 10 segundos porque os 110 segundos extras permitidos pela especificação do problema geralmente não melhoram a solução final encontrada o suficiente para valer a pena esperar.) Portanto, é extremamente idiota. No entanto, ele possui toda a infraestrutura para constituir um bom ponto de partida para uma pesquisa mais inteligente e, se alguém desejar utilizá-la antes do prazo final, incentivo-os a fazê-lo.

O programa começa lendo o dicionário em uma estrutura em árvore. (O formulário não é tão otimizado quanto poderia ser, mas é mais do que suficiente para esses fins.) Após algumas outras inicializações básicas, ele começa a gerar quadros e a classificá-los. O programa examina cerca de 20 mil placas por segundo na minha máquina e, após cerca de 200 mil placas, a abordagem aleatória começa a ficar seca.

Como apenas uma placa está realmente sendo avaliada a qualquer momento, os dados da pontuação são armazenados em variáveis ​​globais. Isso me permite minimizar a quantidade de dados constantes que precisam ser passados ​​como argumentos para as funções recursivas. (Tenho certeza de que isso dará algumas pessoas para as pessoas e peço desculpas.) A lista de palavras é armazenada como uma árvore de pesquisa binária. Todas as palavras encontradas devem ser pesquisadas na lista de palavras, para que palavras duplicadas não sejam contadas duas vezes. A lista de palavras é necessária apenas durante o processo de avaliação, no entanto, é descartada depois que a pontuação é encontrada. Assim, no final do programa, o painel escolhido deve ser pontuado novamente, para que a lista de palavras possa ser impressa.

Curiosidade: a pontuação média de um painel Boggle gerado aleatoriamente, conforme pontuado por english.0, é de 61,7 pontos.


Obviamente, preciso melhorar minha própria eficiência. :-)
Gaffi

Minha abordagem genética atinge cerca de 700-800 pontos, gerando cerca de 200 mil placas, então você está claramente fazendo algo muito melhor do que eu na maneira como produz a próxima geração.
Peter Taylor

Minha própria estrutura em árvore, tendo sido implementada apenas para a lista de palavras mestres até o momento, enquanto funciona e me permite validar placas, atola a memória do sistema (fica ativamente atrasada, levando um tempo considerável para forçar o processo para terminar mais cedo). Certamente isso é culpa minha, mas estou trabalhando nisso! Edit: Já corrigido! ;-)
Gaffi

@ PeterTaylor Pensei em tentar um algoritmo genético, mas não consegui pensar em um mecanismo plausível para combinar duas placas. Como você está fazendo isso? Você está selecionando o pai aleatoriamente para cada slot no tabuleiro?
breadbox

Dividi o estado do tabuleiro na permutação de dados e os rostos aparecendo nos dados. Para o cruzamento de permutação, eu uso "order crossover 1" de cs.colostate.edu/~genitor/1995/permutations.pdf e, para o crossover de face, faço o óbvio. Mas tenho uma ideia para uma abordagem totalmente diferente que preciso encontrar tempo para implementar.
22412 Peter

3

VBA (média atualmente variando de 80 a 110 pontos, inacabada)

Aqui está o meu processo de trabalho, mas está longe de ser o melhor possível; minha melhor pontuação absoluta encontrada em qualquer placa após muitas execuções de teste é de cerca de 120. Ainda precisa haver uma limpeza geral melhor e tenho certeza de que há mais eficiências a serem obtidas em vários locais.

  • 2012.05.09:
    • Postagem original
  • 2012.05.10 - 2012.05.18:
    • Melhorou o algoritmo de pontuação
    • Melhorada a lógica de busca de caminhos
  • 2012.06.07 - 2012.06.12 :
    • Reduzido o limite de palavras para 6 em 8. Permite mais quadros com palavras menores. Parece ter feito uma ligeira melhora na pontuação média. (10 a 15 ou mais placas marcadas por execução vs. 1 a 2)
    • Seguindo a sugestão da breadbox, criei uma estrutura em árvore para abrigar a lista de palavras. Isso acelera significativamente a verificação de back-end das palavras em um quadro.
    • Eu brinquei com a alteração do tamanho máximo das palavras (velocidade x pontuação) e ainda não decidi se 5 ou 6 é uma opção melhor para mim. 6 resultam em 100-120 placas verificadas, enquanto 5 resultam em 500-1000 (ambas ainda muito abaixo dos outros exemplos fornecidos até agora).
    • Problema : Após várias execuções sucessivas, o processo começa a ficar lento, portanto ainda há memória a ser gerenciada.

Isso provavelmente parece horrível para alguns de vocês, mas como eu disse, WIP. Sou muito aberto a críticas construtivas ! Desculpe pelo corpo muito longo ...


Módulo de classe de dados :

Option Explicit

Private Sides() As String

Sub NewDie(NewLetters As String)
    Sides = Split(NewLetters, ",")
End Sub

Property Get Side(i As Integer)
    Side = Sides(i)
End Property

Módulo de classe de árvore :

Option Explicit

Private zzroot As TreeNode


Sub AddtoTree(ByVal TreeWord As Variant)
Dim i As Integer
Dim TempNode As TreeNode

    Set TempNode = TraverseTree(TreeWord, zzroot)
    SetNode TreeWord, TempNode

End Sub

Private Function SetNode(ByVal Value As Variant, parent As TreeNode) As TreeNode
Dim ValChar As String
    If Len(Value) > 0 Then
        ValChar = Left(Value, 1)
        Select Case Asc(ValChar) - 96
            Case 1:
                Set parent.Node01 = AddNode(ValChar, parent.Node01)
                Set SetNode = parent.Node01
            Case 2:
                Set parent.Node02 = AddNode(ValChar, parent.Node02)
                Set SetNode = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set parent.Node26 = AddNode(ValChar, parent.Node26)
                Set SetNode = parent.Node26
            Case Else:
                Set SetNode = Nothing
        End Select

        Set SetNode = SetNode(Right(Value, Len(Value) - 1), SetNode)
    Else
        Set parent.Node27 = AddNode(True, parent.Node27)
        Set SetNode = parent.Node27
    End If
End Function

Function AddNode(ByVal Value As Variant, NewNode As TreeNode) As TreeNode
    If NewNode Is Nothing Then
        Set AddNode = New TreeNode
        AddNode.Value = Value
    Else
        Set AddNode = NewNode
    End If
End Function
Function TraverseTree(TreeWord As Variant, parent As TreeNode) As TreeNode
Dim Node As TreeNode
Dim ValChar As String
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            Set TraverseTree = TraverseTree(Right(TreeWord, Len(TreeWord) - 1), Node)
            If Not TraverseTree Is Nothing Then
                Set TraverseTree = parent
            End If
        Else
            Set TraverseTree = parent
        End If
    Else
        If parent.Node27.Value Then
            Set TraverseTree = parent
        Else
            Set TraverseTree = Nothing
        End If
    End If
End Function

Function WordScore(TreeWord As Variant, Step As Integer, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            WordScore = WordScore(Right(TreeWord, Len(TreeWord) - 1), Step + 1, Node)
        End If
    Else
        If parent.Node27 Is Nothing Then
            WordScore = 0
        Else
            WordScore = Step
        End If
    End If
End Function

Function ValidWord(TreeWord As Variant, Optional parent As TreeNode = Nothing) As Integer
Dim Node As TreeNode
Dim ValChar As String
    If parent Is Nothing Then Set parent = zzroot
    If Len(TreeWord) > 0 Then
        ValChar = Left(TreeWord, 1)

        Select Case Asc(ValChar) - 96
            Case 1:
                Set Node = parent.Node01
            Case 2:
                Set Node = parent.Node02
            ' ... - Reduced to limit size of answer.
            Case 26:
                Set Node = parent.Node26
            Case Else:
                Set Node = Nothing
        End Select

        If Not Node Is Nothing Then
            ValidWord = ValidWord(Right(TreeWord, Len(TreeWord) - 1), Node)
        Else
            ValidWord = False
        End If
    Else
        If parent.Node27 Is Nothing Then
            ValidWord = False
        Else
            ValidWord = True
        End If
    End If
End Function

Private Sub Class_Initialize()
    Set zzroot = New TreeNode
End Sub

Private Sub Class_Terminate()
    Set zzroot = Nothing
End Sub

Módulo de classe TreeNode :

Option Explicit

Public Value As Variant
Public Node01 As TreeNode
Public Node02 As TreeNode
' ... - Reduced to limit size of answer.
Public Node26 As TreeNode
Public Node27 As TreeNode

Módulo principal :

Option Explicit

Const conAllSides As String = ";a,a,e,e,g,n;e,l,r,t,t,y;a,o,o,t,t,w;a,b,b,j,o,o;e,h,r,t,v,w;c,i,m,o,t,u;d,i,s,t,t,y;e,i,o,s,s,t;d,e,l,r,v,y;a,c,h,o,p,s;h,i,m,n,qu,u;e,e,i,n,s,u;e,e,g,h,n,w;a,f,f,k,p,s;h,l,n,n,r,z;d,e,i,l,r,x;"
Dim strBoard As String, strBoardTemp As String, strWords As String, strWordsTemp As String
Dim CheckWordSub As String
Dim iScore As Integer, iScoreTemp As Integer
Dim Board(1 To 4, 1 To 4) As Integer
Dim AllDice(1 To 16) As Dice
Dim AllWordsTree As Tree
Dim AllWords As Scripting.Dictionary
Dim CurWords As Scripting.Dictionary
Dim FullWords As Scripting.Dictionary
Dim JunkWords As Scripting.Dictionary
Dim WordPrefixes As Scripting.Dictionary
Dim StartTime As Date, StopTime As Date
Const MAX_LENGTH As Integer = 5
Dim Points(3 To 8) As Integer

Sub Boggle()
Dim DiceSetup() As String
Dim i As Integer, j As Integer, k As Integer

    StartTime = Now()

    strBoard = vbNullString
    strWords = vbNullString
    iScore = 0

    ReadWordsFileTree

    DiceSetup = Split(conAllSides, ";")

    For i = 1 To 16
        Set AllDice(i) = New Dice
        AllDice(i).NewDie "," & DiceSetup(i)
    Next i

    Do While WithinTimeLimit

        Shuffle

        strBoardTemp = vbNullString
        strWordsTemp = vbNullString
        iScoreTemp = 0

        FindWords

        If iScoreTemp > iScore Or iScore = 0 Then
            iScore = iScoreTemp
            k = 1
            For i = 1 To 4
                For j = 1 To 4
                    strBoardTemp = strBoardTemp & AllDice(k).Side(Board(j, i)) & "  "
                    k = k + 1
                Next j
                strBoardTemp = strBoardTemp & vbNewLine
            Next i
            strBoard = strBoardTemp
            strWords = strWordsTemp

        End If

    Loop

    Debug.Print strBoard
    Debug.Print strWords
    Debug.Print iScore & " points"

    Set AllWordsTree = Nothing
    Set AllWords = Nothing
    Set CurWords = Nothing
    Set FullWords = Nothing
    Set JunkWords = Nothing
    Set WordPrefixes = Nothing

End Sub

Sub ShuffleBoard()
Dim i As Integer

    For i = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        Board(Int((i - 1) / 4) + 1, 4 - (i Mod 4)) = Int(6 * Rnd() + 1)
    Next i

End Sub

Sub Shuffle()
Dim n As Long
Dim Temp As Variant
Dim j As Long

    Randomize
    ShuffleBoard
    For n = 1 To 16
        If Not WithinTimeLimit Then Exit Sub
        j = CLng(((16 - n) * Rnd) + n)
        If n <> j Then
            Set Temp = AllDice(n)
            Set AllDice(n) = AllDice(j)
            Set AllDice(j) = Temp
        End If
    Next n

    Set FullWords = New Scripting.Dictionary
    Set CurWords = New Scripting.Dictionary
    Set JunkWords = New Scripting.Dictionary

End Sub

Sub ReadWordsFileTree()
Dim FSO As New FileSystemObject
Dim FS
Dim strTemp As Variant
Dim iLength As Integer
Dim StartTime As Date

    StartTime = Now()
    Set AllWordsTree = New Tree
    Set FS = FSO.OpenTextFile("P:\Personal\english.txt")

    Points(3) = 1
    Points(4) = 1
    Points(5) = 2
    Points(6) = 3
    Points(7) = 5
    Points(8) = 11

    Do Until FS.AtEndOfStream
        strTemp = FS.ReadLine
        If strTemp = LCase(strTemp) Then
            iLength = Len(strTemp)
            iLength = IIf(iLength > 8, 8, iLength)
            If InStr(strTemp, "'") < 1 And iLength > 2 Then
                AllWordsTree.AddtoTree strTemp
            End If
        End If
    Loop
    FS.Close

End Sub

Function GetScoreTree() As Integer
Dim TempScore As Integer

    If Not WithinTimeLimit Then Exit Function

    GetScoreTree = 0

    TempScore = AllWordsTree.WordScore(CheckWordSub, 0)
    Select Case TempScore
        Case Is < 3:
            GetScoreTree = 0
        Case Is > 8:
            GetScoreTree = 11
        Case Else:
            GetScoreTree = Points(TempScore)
    End Select

End Function

Sub SubWords(CheckWord As String)
Dim CheckWordScore As Integer
Dim k As Integer, l As Integer

    For l = 0 To Len(CheckWord) - 3
        For k = 1 To Len(CheckWord) - l
            If Not WithinTimeLimit Then Exit Sub

            CheckWordSub = Mid(CheckWord, k, Len(CheckWord) - ((k + l) - 1))

            If Len(CheckWordSub) >= 3 And Not CurWords.Exists(CheckWordSub) Then
                CheckWordScore = GetScoreTree

                If CheckWordScore > 0 Then
                    CurWords.Add CheckWordSub, CheckWordSub
                    iScoreTemp = iScoreTemp + CheckWordScore
                    strWordsTemp = strWordsTemp & CheckWordSub & vbNewLine
                End If

                If Left(CheckWordSub, 1) = "q" Then
                    k = k + 1
                End If
            End If

        Next k
    Next l

End Sub

Sub FindWords()
Dim CheckWord As String
Dim strBoardLine(1 To 16) As String
Dim Used(1 To 16) As Boolean
Dim i As Integer, j As Integer, k As Integer, l As Integer, m As Integer, n As Integer
Dim StartSquare As Integer
Dim FullCheck As Variant

    n = 1
    For l = 1 To 4
        For m = 1 To 4
            If Not WithinTimeLimit Then Exit Sub
            strBoardLine(n) = AllDice(n).Side(Board(m, l))
            n = n + 1
        Next m
    Next l

    For i = 1 To 16
        For k = 1 To 16

            If Not WithinTimeLimit Then Exit Sub
            If k Mod 2 = 0 Then
                For j = 1 To 16
                    Used(j) = False
                Next j

                Used(i) = True
                MakeWords strBoardLine, Used, i, k / 2, strBoardLine(i)
            End If

        Next k
    Next i

    For Each FullCheck In FullWords.Items
        SubWords CStr(FullCheck)
    Next FullCheck

End Sub

Function MakeWords(BoardLine() As String, Used() As Boolean, _
    Start As Integer, _
    Direction As Integer, CurString As String) As String
Dim i As Integer, j As Integer, k As Integer, l As Integer

    j = 0

    Select Case Direction
        Case 1:
            k = Start - 5
        Case 2:
            k = Start - 4
        Case 3:
            k = Start - 3
        Case 4:
            k = Start - 1
        Case 5:
            k = Start + 1
        Case 6:
            k = Start + 3
        Case 7:
            k = Start + 4
        Case 8:
            k = Start + 5
    End Select

    If k >= 1 And k <= 16 Then
        If Not WithinTimeLimit Then Exit Function

        If Not Used(k) Then
            If ValidSquare(Start, k) Then
                If Not (JunkWords.Exists(CurString & BoardLine(k))) And Not FullWords.Exists(CurString & BoardLine(k)) Then
                    Used(k) = True
                    For l = 1 To MAX_LENGTH
                        If Not WithinTimeLimit Then Exit Function
                        MakeWords = CurString & BoardLine(k)
                        If Not (JunkWords.Exists(MakeWords)) Then
                            JunkWords.Add MakeWords, MakeWords
                        End If
                        If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
                            FullWords.Add MakeWords, MakeWords
                        ElseIf Len(MakeWords) < MAX_LENGTH Then
                            MakeWords BoardLine, Used, k, l, MakeWords
                        End If
                    Next l
                    Used(k) = False
                End If
            End If
        End If
    End If

    If Len(MakeWords) = MAX_LENGTH And Not FullWords.Exists(MakeWords) Then
        FullWords.Add MakeWords, MakeWords
        Debug.Print "FULL - " & MakeWords
    End If

End Function

Function ValidSquare(StartSquare As Integer, EndSquare As Integer) As Boolean
Dim sx As Integer, sy As Integer, ex As Integer, ey As Integer

    If Not WithinTimeLimit Then Exit Function

    sx = (StartSquare - 1) Mod 4 + 1
    ex = (EndSquare - 1) Mod 4 + 1

    sy = Int((StartSquare - 1) / 4 + 1)
    ey = Int((EndSquare - 1) / 4 + 1)

    ValidSquare = (sx - 1 <= ex And sx + 1 >= ex) And (sy - 1 <= ey And sy + 1 >= ey) And StartSquare <> EndSquare

End Function

Function WithinTimeLimit() As Boolean
    StopTime = Now()
    WithinTimeLimit = (Round(CDbl(((StopTime - StartTime) - Int(StopTime - StartTime)) * 86400), 0) < 120)
End Function

2
Eu não olhei o código, mas 50 pontos são ridiculamente baixos. Joguei tabuleiros gerados aleatoriamente com pontuações acima de 1000 (usando SOWPODS - a lista de palavras fornecida pode ser menos extensa). Convém verificar se há um erro de sinal!
22412 Peter Taylor

@ PeterTaylor Obrigado pela sugestão. Eu sei que a pontuação é demasiado baixo, e eu sei que parte das mentiras problema no fato de que eu posso ver as palavras óbvias sendo perdidas ...
Gaffi

@PeterTaylor Além disso, estou registrando continuamente meu progresso, em vez de esperar pelo meu produto final, já que ninguém mais fez uma tentativa dele ainda. Eu gostaria de manter a questão um pouco viva até que isso aconteça.
Gaffi 16/05

Devo também observar que isso não está sendo executado na máquina mais rápida do mercado, o que também não ajuda.
Gaffi 18/05/12

11
@ Gaaffi 10 segundos para calcular a pontuação? São 9.999 segundos a mais. Você precisa repensar seu código. Se você se recusar a transformar sua lista de palavras em uma árvore, faça pelo menos o seguinte: Faça uma lista (hashtable, qualquer que seja) de todos os prefixos de duas letras. Então, quando você começar a seguir um caminho no quadro, se as duas primeiras letras não estiverem na lista, pule essa subárvore inteira de caminhos possíveis. Novamente, construir a árvore inteira é o melhor, mas a lista de prefixos de duas letras ajudará e é muito barata de fazer.
breadbox

2

Visualização rápida do tamanho do espaço de pesquisa.

   16! => 20922789888000 Dice Permutations
(6^16) =>  2821109907456 Face Permutations
 59025489844657012604928000 Boggle Grids 

Reduzindo para excluir a repetição em cada dado.

              16! => 20922789888000 Dice Permutations
(4^4)*(5^6)*(6^5) => 31104000000 Unique Face Permutations
   650782456676352000000000 Boggle Grids 

@breadbox armazena o dicionário como uma verificação da Tabela Hash O (1).

EDITAR

Melhor Diretor (eu testemunhei até agora)

L  E  A  N
S  E  T  M
T  S  B  D
I  E  G  O

Score: 830
Words: 229
SLEETIEST  MANTELETS
MANTEELS  MANTELET  MATELESS
MANTEEL  MANTELS  TESTEES  BETISES  OBTESTS  OBESEST
SLEETS  SLEEST  TESTIS  TESTES  TSETSE  MANTES  MANTEL  TESTAE  TESTEE
STEELS  STELES  BETELS  BESETS  BESITS  BETISE  BODGES  BESEES  EISELS
GESTES  GEISTS  OBTEST
LEANT  LEATS  LEETS  LEESE  LESES  LESTS  LESBO  ANTES  NATES  SLEET  SETAE
SEATS  STIES  STEEL  STETS  STEAN  STEAM  STELE  SELES  TAELS  TEELS  TESTS
TESTE  TELES  TETES  MATES  TESTA  TEATS  SEELS  SITES  BEETS  BETEL  BETES
BESET  BESTS  BESIT  BEATS  BODGE  BESEE  DOGES  EISEL  GESTS  GESTE  GESSE
GEITS  GEIST  OBESE
LEAN  LEAT  LEAM  LEET  LEES  LETS  LEST  LESS  EATS  EELS  ELSE  ETNA  ESES
ESTS  ESSE  ANTE  ANTS  ATES  AMBO  NATS  SLEE  SEEL  SETA  SETS  SESE  SEAN
SEAT  SEAM  SELE  STIE  STET  SEES  TAEL  TAES  TEEL  TEES  TEST  TEAM  TELE
TELS  TETS  TETE  MATE  MATS  MAES  TIES  TEAT  TEGS  SELS  SEGO  SITS  SITE
BEET  BEES  BETA  BETE  BETS  BEST  BEAN  BEAT  BEAM  BELS  BOGS  BEGO  BEGS
DOGE  DOGS  DOBS  GOBS  GEST  GEIT  GETS  OBES
LEA  LEE  LET  LES  EAN  EAT  EEL  ELS  ETA  EST  ESS  ANT  ATE  NAT  NAE  NAM
SEE  SET  SEA  SEL  TAN  TAE  TAM  TEE  TES  TEA  TEL  TET  MNA  MAN  MAT  MAE
TIE  TIS  TEG  SEG  SEI  SIT  BEE  BET  BEL  BOD  BOG  BEG  DOG  DOB  ITS  EGO
GOD  GOB  GET  OBS  OBE
EA  EE  EL  ET  ES  AN  AT  AE  AM  NA  ST  TA  TE  MA
TI  SI  BE  BO  DO  IT  IS  GO  OD  OB

Me arranje uma máquina com tanta memória RAM e conversaremos.
breadbox

Você precisa dividir as permutações dos dados por 8 para dar conta das simetrias do quadrado. Além disso, como você obtém (4 ^ 4) (5 ^ 6) (6 ^ 5)? Eu faço isso (4 ^ 3) (5 ^ 7) (6 ^ 6), para um espaço total de busca de pouco mais de 2 ^ 79.
Peter Taylor

@ Peter Taylor: Você está certo. Devo ter excluído um para muitos, quando as faces únicas. Acho que podemos concordar que há 83 faces únicas (excluindo repetições no dado). Escolha 16 sem repetições. '83 x 82 x 81 x 80 x 79 x 78 x 77 x 76 x 75 x 74 x 73 x 72 x 71 x 70 x 69 x 68 'Aprox: 1,082 x (10 ^ 30) ==> ~ 2 ^ 100 seja como for, é um grande número.
Adam Speight

2
@ AdamSpeight Inicialmente, assumi que seu comentário sobre o armazenamento do dicionário como uma hashtable era apenas uma piada, então eu basicamente o ignorei. Me desculpe. Uma resposta adequada seria: Na verdade, uma hashtable é uma péssima estrutura de dados para esse problema. Ele só pode responder à pergunta "X é uma palavra válida?", Então você deve criar todas as seqüências possíveis para encontrar as palavras. Um DAWG permite que você pergunte "X é um prefixo de qualquer palavra válida? E, nesse caso, que letras podem segui-lo?" Isso permite que você apare o espaço de pesquisa em uma fração minúscula do seu tamanho total.
breadbox

O Hashtable é terrível, pois impede que você selecione fragmentos de palavras que nunca se tornarão palavras completas (dicttree.ceiling (fragmento). Começa com (fragmento)). Embora qualquer painel de boggle tenha muitos milhões de palavras em potencial, você pode jogar uma grande parte delas depois que duas ou três letras estiverem juntas. O percurso da árvore é mais lento que a pesquisa por hashtable, mas a árvore permite que você evite mais de 99% do trabalho através do retorno.
Jim W

1

Minha entrada está aqui no Dream.In.Code ~ 30ms por uma pesquisa de placa (em uma máquina com 2 núcleos, deve ser mais rápida com mais núcleos)


Ainda olhando para ele, mas o seu primeiro link na página está faltando o :no http://. ;-)
Gaffi 18/06/12

Muito agradável. Vou tentar roubar isso para mim como uma experiência de aprendizado. .NETa VBAnão é muito difícil.
Gaffi

Você se importaria de atualizar a resposta para incluir sua pontuação média ao executar a lista ISPELL (não SOWPODS)? Isso faz parte do desafio e estou interessado em ver como seus resultados se comparam aos da caixa de pão.
Gaffi
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.