Giocatore più veloce per punti e scatole


16

La sfida è quella di scrivere un risolutore per il classico gioco di matita e carta Dots and Boxes . Il tuo codice dovrebbe prendere due numeri interi me ncome input che specifica la dimensione della scheda.

A partire da una griglia vuota di punti, i giocatori si alternano, aggiungendo una singola linea orizzontale o verticale tra due punti adiacenti non uniti. Un giocatore che completa il quarto lato di una scatola 1 × 1 guadagna un punto e fa un altro turno. (I punti vengono in genere registrati inserendo nella casella un segno identificativo del giocatore, come una iniziale). Il gioco termina quando non è possibile posizionare più linee. Il vincitore del gioco è il giocatore con il maggior numero di punti.

inserisci qui la descrizione dell'immagine

Si può supporre che sia n = mo n = m - 1e mè di almeno 2.

La sfida è quella solvedel gioco Dots and Boxes più grande possibile in meno di un minuto. La dimensione di un gioco è semplicemente n*m. L'output del tuo codice dovrebbe essere win, drawo losequale dovrebbe essere il risultato per il primo giocatore supponendo che entrambi i giocatori giochino in modo ottimale.

Il tuo codice deve essere compilabile / eseguibile su Ubuntu utilizzando strumenti facilmente installabili e gratuiti. Segnala il tuo punteggio come l'area più grande che puoi risolvere sul tuo computer in 1 minuto insieme al tempo. Proverò quindi il codice sul mio computer e creerò una tabella delle voci ordinate.

In caso di pareggio, il vincitore sarà il codice più veloce sulla scheda di dimensioni maggiori che può risolvere in meno di un minuto.


Sarebbe meglio se il codice prodotto non solo vincesse o perdesse, ma anche il punteggio effettivo. Ciò consente un controllo di integrità della correttezza.


2
Dobbiamo usare minimax?
qwr

@qwr Puoi farmi sapere quale altra opzione avevi in ​​mente?

Aspetta, c'è un vincitore prevedibile in questo gioco basato esclusivamente sulla dimensione della griglia?
Non che Charles,

@Charles Sì se entrambi i giocatori giocano in modo ottimale.

1
@PeterTaylor Penso che tu ottenga due punti ma solo un turno extra.

Risposte:


15

C99 - scheda 3x3 in 0,084s

Modifica: ho riformattato il mio codice e fatto un'analisi più approfondita dei risultati.

Ulteriori modifiche: aggiunta potatura per simmetrie. Questo rende 4 configurazioni dell'algoritmo: con o senza simmetrie X con o senza potatura alfa-beta

Modifiche più lontane: aggiunta la memoizzazione usando una tabella hash, ottenendo finalmente l'impossibile: risolvere una scheda 3x3!

Caratteristiche primarie:

  • implementazione diretta di minimax con potatura alfa-beta
  • pochissima gestione della memoria (mantiene la dll di mosse valide; O (1) aggiornamenti per ramo nella ricerca dell'albero)
  • secondo file con potatura per simmetrie. Ottiene ancora aggiornamenti O (1) per ramo (tecnicamente O (S) dove S è il numero di simmetrie. Questo è 7 per le schede quadrate e 3 per le schede non quadrate)
  • il terzo e il quarto file aggiungono memoization. Hai il controllo sulla dimensione dell'hashtable ( #define HASHTABLE_BITWIDTH). Quando questa dimensione è maggiore o uguale al numero di pareti, non garantisce collisioni e aggiornamenti O (1). Gli hashtable più piccoli avranno più collisioni e saranno leggermente più lenti.
  • compilare con -DDEBUGper le stampe

Potenziali miglioramenti:

  • risolto il problema con la perdita di memoria ridotta nella prima modifica
  • potatura alfa / beta aggiunta nella seconda modifica
  • simmetrie di potatura aggiunte nella terza modifica (si noti che le simmetrie non sono gestite dalla memoizzazione, quindi rimane un'ottimizzazione separata.)
  • memoization aggiunta nella 4a modifica
  • attualmente la memoization utilizza un bit indicatore per ciascun muro. Una scheda 3x4 ha 31 pareti, quindi questo metodo non è in grado di gestire schede 4x4 indipendentemente dai vincoli temporali. il miglioramento sarebbe emulare gli interi X-bit, dove X è almeno pari al numero di muri.

Codice

A causa della mancanza di organizzazione, il numero di file è cresciuto fuori controllo. Tutto il codice è stato spostato in questo repository Github . Nella modifica della memoization, ho aggiunto un makefile e uno script di test.

risultati

Traccia log dei tempi di esecuzione

Note sulla complessità

Gli approcci a forza bruta a punti e scatole esplodono in complessità molto rapidamente .

Considera una tavola con Rrighe e Ccolonne. Ci sono R*Cquadrati, R*(C+1)pareti verticali e C*(R+1)pareti orizzontali. Questo è un totale di W = 2*R*C + R + C.

Poiché Lembik ci ha chiesto di risolvere il gioco con minimax, dobbiamo attraversare le foglie dell'albero del gioco. Ignoriamo la potatura per ora, perché ciò che conta sono gli ordini di grandezza.

Ci sono Wopzioni per la prima mossa. Per ognuno di questi, il giocatore successivo può giocare a qualsiasi delle W-1pareti rimanenti, ecc. Questo ci dà uno spazio di ricerca di SS = W * (W-1) * (W-2) * ... * 1, o SS = W!. I fattoriali sono enormi, ma è solo l'inizio. SSè il numero di nodi foglia nello spazio di ricerca. Più rilevante per la nostra analisi è il numero totale di decisioni che dovevano essere prese (cioè il numero di rami B nella struttura). Il primo strato di rami ha Wopzioni. Per ognuno di questi, il livello successivo ha W-1, ecc.

B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!

B = SUM W!/(W-k)!
  k=0..W-1

Diamo un'occhiata ad alcune dimensioni di piccoli tavoli:

Board Size  Walls  Leaves (SS)      Branches (B)
---------------------------------------------------
1x1         04     24               64
1x2         07     5040             13699
2x2         12     479001600        1302061344
2x3         17     355687428096000  966858672404689

Questi numeri stanno diventando ridicoli. Almeno spiegano perché il codice della forza bruta sembra rimanere per sempre su una scheda 2x3. Lo spazio di ricerca di una scheda 2x3 è 742560 volte più grande di 2x2 . Se il completamento di 2x2 richiede 20 secondi, un'estrapolazione conservativa prevede oltre 100 giorni di tempo di esecuzione per 2x3. Chiaramente dobbiamo potare.

Analisi di potatura

Ho iniziato aggiungendo una potatura molto semplice usando l'algoritmo alpha-beta. Fondamentalmente, smette di cercare se un avversario ideale non gli darebbe mai le sue attuali opportunità. "Ehi guarda, vinco di molto se il mio avversario mi permette di ottenere tutti i quadrati!", Mai pensato AI.

modifica Ho anche aggiunto la potatura basata su pannelli simmetrici. Non uso un approccio di memoization, nel caso in cui un giorno aggiungessi memoization e voglio mantenere quell'analisi separata. Invece, funziona così: la maggior parte delle linee ha una "coppia simmetrica" ​​da qualche altra parte sulla griglia. Esistono fino a 7 simmetrie (orizzontale, verticale, 180 rotazione, 90 rotazione, 270 rotazione, diagonale e l'altra diagonale). Tutti e 7 si applicano alle schede quadrate, ma le ultime 4 non si applicano alle schede non quadrate. Ogni muro ha un puntatore alla sua "coppia" per ognuna di queste simmetrie. Se, andando in un turno, la scacchiera è simmetrica orizzontalmente, allora solo una di ciascuna coppia orizzontale deve essere giocata.

modifica modifica Memoization! Ogni muro ottiene un ID univoco, che ho opportunamente impostato per essere un bit indicatore; l'ennesimo muro ha l'id 1 << n. L'hash di una tavola, quindi, è solo l'OR di tutte le pareti giocate. Questo viene aggiornato ad ogni filiale in O (1) volta. La dimensione dell'hashtable è impostata in a #define. Tutti i test sono stati eseguiti con dimensioni 2 ^ 12, perché perché no? Quando ci sono più muri che bit che indicizzano la tabella hash (12 bit in questo caso), i 12 meno significativi vengono mascherati e usati come indice. Le collisioni vengono gestite con un elenco collegato in ciascun indice hashtable. Il seguente grafico è la mia analisi rapida e dettagliata di come le dimensioni hashtable influiscono sulle prestazioni. Su un computer con RAM infinita, impostiamo sempre le dimensioni della tabella sul numero di muri. Una scheda 3x4 avrebbe una lunghezza hash di 2 ^ 31. Purtroppo non abbiamo quel lusso.

Effetti della dimensione Hashtable

Ok, torniamo alla potatura .. Fermando la ricerca in alto nell'albero, possiamo risparmiare molto tempo non scendendo alle foglie. Il "fattore di potatura" è la frazione di tutti i rami possibili che abbiamo dovuto visitare. La forza bruta ha un fattore di potatura di 1. Più è piccola, meglio è.

Trama del registro dei rami presi

Trama del log dei fattori di potatura


23s sembra visibilmente lento per un linguaggio veloce come C. Sei un forzante brutale?
qwr

Forza bruta con una piccola quantità di potatura da alpha beta. Sono d'accordo che 23s è sospetto, ma non vedo alcuna ragione nel mio codice che sarebbe incoerente .. In altre parole, è un mistero
wrongu

1
l'input è formattato come specificato dalla domanda. due numeri interi separati da spazio che rows columnsspecificano le dimensioni della scheda
wrongu

1
@Lembik Non penso che ci sia ancora niente da fare. Ho finito con questo folle progetto!
wrongu,

1
Penso che la tua risposta meriti un posto speciale. Ho cercato e 3 per 3 è la dimensione del problema più grande che sia mai stata risolta prima e il tuo codice è quasi istantaneo. Se riesci a risolvere 3 per 4 o 4 per 4 potresti aggiungere il risultato alla pagina wiki ed essere famoso :)

4

Python - 2x2 in 29s

Cross-posting dai puzzle . Non particolarmente ottimizzato, ma può essere un utile punto di partenza per altri partecipanti.

from collections import defaultdict

VERTICAL, HORIZONTAL = 0, 1

#represents a single line segment that can be drawn on the board.
class Line(object):
    def __init__(self, x, y, orientation):
        self.x = x
        self.y = y
        self.orientation = orientation
    def __hash__(self):
        return hash((self.x, self.y, self.orientation))
    def __eq__(self, other):
        if not isinstance(other, Line): return False
        return self.x == other.x and self.y == other.y and self.orientation == other.orientation
    def __repr__(self):
        return "Line({}, {}, {})".format(self.x, self.y, "HORIZONTAL" if self.orientation == HORIZONTAL else "VERTICAL")

class State(object):
    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.whose_turn = 0
        self.scores = {0:0, 1:0}
        self.lines = set()
    def copy(self):
        ret = State(self.width, self.height)
        ret.whose_turn = self.whose_turn
        ret.scores = self.scores.copy()
        ret.lines = self.lines.copy()
        return ret
    #iterate through all lines that can be placed on a blank board.
    def iter_all_lines(self):
        #horizontal lines
        for x in range(self.width):
            for y in range(self.height+1):
                yield Line(x, y, HORIZONTAL)
        #vertical lines
        for x in range(self.width+1):
            for y in range(self.height):
                yield Line(x, y, VERTICAL)
    #iterate through all lines that can be placed on this board, 
    #that haven't already been placed.
    def iter_available_lines(self):
        for line in self.iter_all_lines():
            if line not in self.lines:
                yield line

    #returns the number of points that would be earned by a player placing the line.
    def value(self, line):
        assert line not in self.lines
        all_placed = lambda seq: all(l in self.lines for l in seq)
        if line.orientation == HORIZONTAL:
            #lines composing the box above the line
            lines_above = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   VERTICAL),   #left
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            #lines composing the box below the line
            lines_below = [
                Line(line.x,   line.y-1, HORIZONTAL), #bottom
                Line(line.x,   line.y-1, VERTICAL),   #left
                Line(line.x+1, line.y-1, VERTICAL),   #right
            ]
            return all_placed(lines_above) + all_placed(lines_below)
        else:
            #lines composing the box to the left of the line
            lines_left = [
                Line(line.x-1, line.y+1, HORIZONTAL), #top
                Line(line.x-1, line.y,   HORIZONTAL), #bottom
                Line(line.x-1, line.y,   VERTICAL),   #left
            ]
            #lines composing the box to the right of the line
            lines_right = [
                Line(line.x,   line.y+1, HORIZONTAL), #top
                Line(line.x,   line.y,   HORIZONTAL), #bottom
                Line(line.x+1, line.y,   VERTICAL),   #right
            ]
            return all_placed(lines_left) + all_placed(lines_right)

    def is_game_over(self):
        #the game is over when no more moves can be made.
        return len(list(self.iter_available_lines())) == 0

    #iterates through all possible moves the current player could make.
    #Because scoring a point lets a player go again, a move can consist of a collection of multiple lines.
    def possible_moves(self):
        for line in self.iter_available_lines():
            if self.value(line) > 0:
                #this line would give us an extra turn.
                #so we create a hypothetical future state with this line already placed, and see what other moves can be made.
                future = self.copy()
                future.lines.add(line)
                if future.is_game_over(): 
                    yield [line]
                else:
                    for future_move in future.possible_moves():
                        yield [line] + future_move
            else:
                yield [line]

    def make_move(self, move):
        for line in move:
            self.scores[self.whose_turn] += self.value(line)
            self.lines.add(line)
        self.whose_turn = 1 - self.whose_turn

    def tuple(self):
        return (tuple(self.lines), tuple(self.scores.items()), self.whose_turn)
    def __hash__(self):
        return hash(self.tuple())
    def __eq__(self, other):
        if not isinstance(other, State): return False
        return self.tuple() == other.tuple()

#function decorator which memorizes previously calculated values.
def memoized(fn):
    answers = {}
    def mem_fn(*args):
        if args not in answers:
            answers[args] = fn(*args)
        return answers[args]
    return mem_fn

#finds the best possible move for the current player.
#returns a (move, value) tuple.
@memoized
def get_best_move(state):
    cur_player = state.whose_turn
    next_player = 1 - state.whose_turn
    if state.is_game_over():
        return (None, state.scores[cur_player] - state.scores[next_player])
    best_move = None
    best_score = float("inf")
    #choose the move that gives our opponent the lowest score
    for move in state.possible_moves():
        future = state.copy()
        future.make_move(move)
        _, score = get_best_move(future)
        if score < best_score:
            best_move = move
            best_score = score
    return [best_move, -best_score]

n = 2
m = 2
s = State(n,m)
best_move, relative_value = get_best_move(s)
if relative_value > 0:
    print("win")
elif relative_value == 0:
    print("draw")
else:
    print("lose")

Può essere velocizzato fino a 18 secondi usando pypy.

2

Javascript - Scheda 1x2 in 20ms

Demo online disponibile qui (avviso: molto lento se superiore a 1x2 con profondità di ricerca completa ): https://dl.dropboxusercontent.com/u/141246873/minimax/index.html

È stato sviluppato per i criteri di vittoria originali (codice golf) e non per la velocità.

Testato su Google Chrome V35 su Windows 7.

//first row is a horizontal edges and second is vertical
var gameEdges = [
    [false, false],
    [false, false, false],
    [false, false]
]

//track all possible moves and score outcome
var moves = []

function minimax(edges, isPlayersTurn, prevScore, depth) {

    if (depth <= 0) {
        return [prevScore, 0, 0];
    }
    else {

        var pointValue = 1;
        if (!isPlayersTurn)
            pointValue = -1;

        var moves = [];

        //get all possible moves and scores
        for (var i in edges) {
            for (var j in edges[i]) {
                //if edge is available then its a possible move
                if (!edges[i][j]) {

                    //if it would result in game over, add it to the scores array, otherwise, try the next move
                    //clone the array
                    var newEdges = [];
                    for (var k in edges)
                        newEdges.push(edges[k].slice(0));
                    //update state
                    newEdges[i][j] = true;
                    //if closing this edge would result in a complete square, get another move and get a point
                    //square could be formed above, below, right or left and could get two squares at the same time

                    var currentScore = prevScore;
                    //vertical edge
                    if (i % 2 !== 0) {//i === 1
                        if (newEdges[i] && newEdges[i][j - 1] && newEdges[i - 1] && newEdges[i - 1][j - 1] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j - 1])
                            currentScore += pointValue;
                        if (newEdges[i] && newEdges[i][parseInt(j) + 1] && newEdges[i - 1] && newEdges[i - 1][j] && newEdges[parseInt(i) + 1] && newEdges[parseInt(i) + 1][j])
                            currentScore += pointValue;
                    } else {//horizontal
                        if (newEdges[i - 2] && newEdges[i - 2][j] && newEdges[i - 1][j] && newEdges[i - 1][parseInt(j) + 1])
                            currentScore += pointValue;
                        if (newEdges[parseInt(i) + 2] && newEdges[parseInt(i) + 2][j] && newEdges[parseInt(i) + 1][j] && newEdges[parseInt(i) + 1][parseInt(j) + 1])
                            currentScore += pointValue;
                    }

                    //leaf case - if all edges are taken then there are no more moves to evaluate
                    if (newEdges.every(function (arr) { return arr.every(Boolean) })) {
                        moves.push([currentScore, i, j]);
                        console.log("reached end case with possible score of " + currentScore);
                    }
                    else {
                        if ((isPlayersTurn && currentScore > prevScore) || (!isPlayersTurn && currentScore < prevScore)) {
                            //gained a point so get another turn
                            var newMove = minimax(newEdges, isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        } else {
                            //didnt gain a point - opponents turn
                            var newMove = minimax(newEdges, !isPlayersTurn, currentScore, depth - 1);

                            moves.push([newMove[0], i, j]);
                        }
                    }



                }


            }

        }//end for each move

        var bestMove = moves[0];
        if (isPlayersTurn) {
            for (var i in moves) {
                if (moves[i][0] > bestMove[0])
                    bestMove = moves[i];
            }
        }
        else {
            for (var i in moves) {
                if (moves[i][0] < bestMove[0])
                    bestMove = moves[i];
            }
        }
        return bestMove;
    }
}

var player1Turn = true;
var squares = [[0,0],[0,0]]//change to "A" or "B" if square won by any of the players
var lastMove = null;

function output(text) {
    document.getElementById("content").innerHTML += text;
}

function clear() {
    document.getElementById("content").innerHTML = "";
}

function render() {
    var width = 3;
    if (document.getElementById('txtWidth').value)
        width = parseInt(document.getElementById('txtWidth').value);
    if (width < 2)
        width = 2;

    clear();
    //need to highlight the last move taken and show who has won each square
    for (var i in gameEdges) {
        for (var j in gameEdges[i]) {
            if (i % 2 === 0) {
                if(j === "0")
                    output("*");
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output(" <b>-</b> ");
                else if (gameEdges[i][j])
                    output(" - ");
                else
                    output("&nbsp;&nbsp;&nbsp;");
                output("*");
            }
            else {
                if (gameEdges[i][j] && lastMove[1] == i && lastMove[2] == j)
                    output("<b>|</b>");
                else if (gameEdges[i][j])
                    output("|");
                else
                    output("&nbsp;");

                if (j <= width - 2) {
                    if (squares[Math.floor(i / 2)][j] === 0)
                        output("&nbsp;&nbsp;&nbsp;&nbsp;");
                    else
                        output("&nbsp;" + squares[Math.floor(i / 2)][j] + "&nbsp;");
                }
            }
        }
        output("<br />");

    }
}

function nextMove(playFullGame) {
    var startTime = new Date().getTime();
    if (!gameEdges.every(function (arr) { return arr.every(Boolean) })) {

        var depth = 100;
        if (document.getElementById('txtDepth').value)
            depth = parseInt(document.getElementById('txtDepth').value);

        if (depth < 1)
            depth = 1;

        var move = minimax(gameEdges, true, 0, depth);
        gameEdges[move[1]][move[2]] = true;
        lastMove = move;

        //if a square was taken, need to update squares and whose turn it is

        var i = move[1];
        var j = move[2];
        var wonSquare = false;
        if (i % 2 !== 0) {//i === 1
            if (gameEdges[i] && gameEdges[i][j - 1] && gameEdges[i - 1] && gameEdges[i - 1][j - 1] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j - 1]) {
                squares[Math.floor(i / 2)][j - 1] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i] && gameEdges[i][parseInt(j) + 1] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        } else {//horizontal
            if (gameEdges[i - 2] && gameEdges[i - 2][j] && gameEdges[i - 1] && gameEdges[i - 1][j] && gameEdges[i - 1] && gameEdges[i - 1][parseInt(j) + 1]) {
                squares[Math.floor((i - 1) / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
            if (gameEdges[i + 2] && gameEdges[parseInt(i) + 2][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][j] && gameEdges[parseInt(i) + 1] && gameEdges[parseInt(i) + 1][parseInt(j) + 1]) {
                squares[Math.floor(i / 2)][j] = player1Turn ? "A" : "B";
                wonSquare = true;
            }
        }

        //didnt win a square so its the next players turn
        if (!wonSquare)
            player1Turn = !player1Turn;

        render();

        if (playFullGame) {
            nextMove(playFullGame);
        }
    }

    var endTime = new Date().getTime();
    var executionTime = endTime - startTime;
    document.getElementById("executionTime").innerHTML = 'Execution time: ' + executionTime;
}

function initGame() {

    var width = 3;
    var height = 2;

    if (document.getElementById('txtWidth').value)
        width = document.getElementById('txtWidth').value;
    if (document.getElementById('txtHeight').value)
        height = document.getElementById('txtHeight').value;

    if (width < 2)
        width = 2;
    if (height < 2)
        height = 2;

    var depth = 100;
    if (document.getElementById('txtDepth').value)
        depth = parseInt(document.getElementById('txtDepth').value);

    if (depth < 1)
        depth = 1;

    if (width > 2 && height > 2 && !document.getElementById('txtDepth').value)
        alert("Warning. Your system may become unresponsive. A smaller grid or search depth is highly recommended.");

    gameEdges = [];
    for (var i = 0; i < height; i++) {
        if (i == 0) {
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i].push(false);
            }
        }
        else {
            gameEdges.push([]);
            for (var j = 0; j < width; j++) {
                gameEdges[(i * 2) - 1].push(false);
            }
            gameEdges.push([]);
            for (var j = 0; j < (width - 1) ; j++) {
                gameEdges[i*2].push(false);
            }
        }
    }

    player1Turn = true;

    squares = [];
    for (var i = 0; i < (height - 1) ; i++) {
        squares.push([]);
        for (var j = 0; j < (width - 1); j++) {
            squares[i].push(0);
        }
    }

    lastMove = null;

    render();
}

document.addEventListener('DOMContentLoaded', initGame, false);

La demo è davvero fantastica! Il 3 x 3 è davvero interessante in quanto il vincitore cambia avanti e indietro mentre aumenti la profondità di ricerca. Posso controllare, il tuo minimox si ferma mai a metà della curva? Quello che voglio dire è che se qualcuno ottiene un quadrato, si estende sempre alla fine del suo turno?

2x2 è 3 punti per 3. Sei sicuro che il tuo codice possa risolverlo esattamente in 20ms?

"se qualcuno ottiene un quadrato, si estende sempre alla fine del suo turno?" - Se il giocatore ottiene un quadrato, passa comunque al turno successivo, ma quel turno successivo è per lo stesso giocatore, cioè ottengono un turno extra per completare un quadrato. "2x2 è 3 punti per 3" - Whoops. In quel caso il mio punteggio è 1x1.
rans
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.