Kan ett datorprogram spela solitär? Malin Persson malpe323@student.liu.se 1
Sammanfattning Solitär är ett gammalt och välkänt brädspel med enkla regler och är därför ett tacksamt spel att försöka lösa maskinellt. I denna rapport beskriver jag tillvägagångssätt som man tidigare har haft för maskinell lösning av solitär, samt mitt eget försök att i programspråket Python konstruera en agent som kan spela solitär. Avslutningsvis jämför jag min agent med de som tidigare implementerats. 2
Innehållsförteckning Kan ett datorprogram spela solitär?...1 1. Inledning...4 1.1 Bakgrund...4 1.2 Regler för solitär...4 1.3 Syfte och metod...5 2. En tidigare maskinell implementation av solitär...5 2.1 Modeller...5 2.1.1 Modell A...5 2.1.2 Modell B...6 2.1.3 Modell C...7 2.2 Pagodafunktioner...7 2.3 Symmetrier och lösningar...7 2.4 Tidsåtgång för de olika modellerna...8 3. Min maskinella implementation av solitär...8 3.1 Kort beskrivning av programmets uppbyggnad...8 3.2 Klassen Node...8 3.3 Klassen Problem...9 3.4 Sökalgoritmer...10 3.5 Resultat...10 5. Diskussion...11 6. Referenslista...12 Tryckta källor...12 Internet...12 3
1. Inledning 1.1 Bakgrund Solitär är ett brädspel för en spelare. Det finns olika varianter av solitär där spelplanen och reglerna kan se lite olika ut. Men gemensamt för alla varianter är att man har en spelplan med ett antal hål, som vart och ett antingen kan vara tomt eller innehålla en kula. Figur 1. Olika varianter av solitär. Från vänster: engelsk solitär, fransk solitär och triangelsolitär. 1.2 Regler för solitär Spelet börjar med att man har kulor i vissa hål på spelplanen och dess vanligaste mål är att endast ha en kula kvar på spelplanen. Vissa varianter av solitär kräver att den sista kulan ska vara i ett visst hål när spelet är slut, till exempel i det mittersta hålet. Det finns också varianter där man väljer ut en specifik kula som ska vara ensam kvar och varianter där man ska ha flera kulor kvar i ett bestämt mönster. I min implementation som presenteras i kapitel 3, samt i den artikel jag har använt som referens (Jefferson m.fl. 2005), används spelplanen för engelsk solitär, där spelplanen har 33 hål arrangerade i ett korsformat mönster. Från början ligger det kulor i alla hål utom i det mittersta, och spelets mål är att bara ha en kula kvar på spelplanen. För att få ta bort en kula från spelplanen måste man hoppa över den med en annan kula, vertikalt eller horisontellt. Man får bara hoppa över en kula per hopp, så för att få göra ett drag krävs det att man har tre hål i rad där de två första hålen har kulor i sig och det tredje är tomt. Efter ett hopp blir de tre hålens tillstånd inverterade, dvs. de två första hålen blir tomma och det tredje får en kula i sig (se Figur 2). Figur 2: Ett drag i solitär Man får inte flytta en kula utan att hoppa över någon kula. Det gäller därför att planera sina drag så att man håller ihop gruppen dvs. så att ingen kula hamnar för långt ifrån de andra. Då kan man aldrig hoppa över den kulan och får inte bort den från spelplanen. 4
1.3 Syfte och metod Syftet är att bygga ett program i programspråket Python som med hjälp av en sökalgoritm löser brädspelet engelsk solitär, samt att jämföra detta med implementationer av engelsk solitär som andra har gjort. I kapitel 2 presenteras och förklaras en implementation av solitär som gjorts av andra (Jefferson m.fl., 2005) och i kapitel 3 presenterar jag min egen implementation. På grund av kursens omfattning och på grund av intresset att jämföra avancerade metoder med enklare, valde jag att endast bygga en enklare agent som löser problemet med vanlig djupet först-sökning i stället för att basera den på den mer optimerade och genomarbetade modell som Jefferson m.fl. (2005) beskriver. På så sätt blir möjligheten större att jämföra de tidigare implementationerna med min agent och på så vis få praktisk insikt i hur mycket effektivitet man vinner genom att använda de avancerade metoderna. 2. En tidigare maskinell implementation av solitär I artikeln Modelling and solvning English Peg Solitaire (Jefferson m.fl., 2005) förklaras hur man kan lösa solitär som ett optimeringsproblem. 2.1 Modeller Reglerna för solitär beskrivs i tre olika matematiska modeller som de kallar modell A, B och C där modell C är en kombination av modell A och B. De tar även upp en slags funktioner som kallas pagodafunktioner, diskuterar hur man kan bryta symmetrier som uppstår i spelet och jämför slutligen resultaten de fick med hjälp av de tre olika modellerna. 2.1.1 Modell A I modell A ställs ett antal villkor upp i form av matematiska ekvationer, som beskriver hur kulor får flyttas i solitär. För att veta vilka hål man har kulor i och inte, använder man variabeln bstate[i, j, t], som beskriver tillståndet för hålet på position (i, j) vid tidpunkten t. bstate[] är lika med 1 om det är en kula i hålet och 0 om hålet är tomt. Ekvation 2-13 förklarar, att för att kunna göra ett drag i solitär måste det finnas tre hål i rad där de två första hålen har kulor i sig och det tredje är tomt. Ekvationerna vid nummer 14 visar att spelplanen vid tidpunkterna t respektive t+1 måste stämma överens med det drag man gör i t. Ekvation 15 implicerar att det endast kan ske ett drag per tidpunkt och ekvation 1 utgör kravet att den sista kulan måste vara i hålet i mitten av spelplanen när spelet är slut. 5
2.1.2 Modell B I modell B används en lista, moves[t], för att visa den sekvens av drag som löser spelet. I Tabell 1 visas de 76 möjliga drag som man kan göra i solitär. Tabell 1. Alla möjliga drag i solitär (Jefferson m.fl., 2005) De ställer även upp villkor (preconditions) som motsvarar spelreglerna. Ett av villkoren är: för att man ska kunna göra ett drag, ska det vara en kula i de två första hålen men inte i det tredje. Vill man exempelvis göra drag nummer 0 i moves[] (alltså 2,0 4,0) måste det vara en kula i 2,0 och 3,0 medan hål 4,0 måste vara tomt. Villkoren kontrolleras genom att undersöka de senaste drag man gjort som inkluderar de aktuella hålen. Om dessa senaste drag har lämnat hålen i tillstånden kula, kula respektive tomt, kan draget genomföras. Två funktioner som heter Support() och Conflict() används för att kontrollera om de olika dragen i moves[] är förenliga 6
med det tillstånd spelplanen är i. Denna modell innehåller väldigt många begränsningar (constraints). Eftersom det finns 76 möjliga drag, det krävs 31 drag för att klara spelet och varje drag kräver att tre villkor är uppfyllda, blir det 7068 begränsningar. 2.1.3 Modell C Modell C är en kombination av modell A och B som använder både listan bstate[] från modell A och moves[] från modell B. Detta gör det möjligt att definiera spelplanens nästa tillstånd med hjälp av det nuvarande tillståndet och ett drag genom att ställa upp ekvivalenser mellan ekvationer med bstate[] och ekvationer med moves[]. För varje hål vid varje tidpunkt kontrollerar man om hålet har samma tillstånd som det hade vid förra tidpunkten eller om det har ändrats. För att göra detta används funktionerna Changes(i, j), som är listan av de drag som kan ändra tillståndet för ett hål; PegIn(i, j), som är de drag som lägger en kula i ett tomt hål och PegOut(i, j) som tar bort en kula från ett fyllt hål. 2.2 Pagodafunktioner För att lösa problemet med hjälp av modellerna ovan, använder sig Jefferson m.fl. (2005) av ett slags funktioner som kallas pagodafunktioner. En pagodafunktion kan se ut på lite olika sätt, men de innebär att man tilldelar varje hål ett värde, så att tre hål a, b, c som sitter i rad alltid uppfyller a+b>=c. Pagodavärdet för spelplanens tillstånd räknas ut genom att summera dessa värden för alla hål som innehåller kulor. Ett villkor i solitär är att pagodavärdet aldrig får öka, bara minska. Om man någon gång mitt i spelet får ett pagodavärde som är lägre än måltillståndets pagodavärde, kommer man inte att kunna lösa spelet. Pagodafunktioner är därför en bra pruningmetod som kan användas för att upptäcka återvändsgränder innan man kommer fram till dem. 2.3 Symmetrier och lösningar En spelplan för engelsk solitär är både rotationssymmetrisk och reflektionssymmetrisk vilket gör att det ofta finns flera olika sätt att göra samma sak. Detta gör sökträdet onödigt stort, och det finns sätt att förminska det. En slags symmetri som man gärna vill ha bort är symmetri mellan oberoende drag. Varje drag påverkar tillstånden för tre hål genom att invertera tillstånden hos dem. Två drag är oberoende av varandra om och endast om deras tre hål inte överlappar varandra. Drag som är oberoende av varandra kan utföras i vilken ordning som helst utan att resultatet påverkas, så man sparar en hel del tid om man bestämmer sig för en ordning. För att göra detta ordnar man alla möjliga drag i nummerordning enligt tabellen i modell B och inför begränsningen att alla par av oberoende drag måste utföras i nummerordning med det lägsta först. En annan sorts symmetri är den när flera olika sekvenser av drag uppnår samma resultat. För att bryta denna symmetri kan man utföra en komplett sökning ner till ett visst djup, gruppera de sekvenser av drag som leder till identiska tillstånd och sedan införa begränsningar som endast tillåter en representant från varje grupp. 7
2.4 Tidsåtgång för de olika modellerna Modell A, B och C jämförs i förmåga att lösa central solitär (dvs. solitär då kulan måste vara i mitten i måltillståndet). Modell A misslyckades med att lösa problemet inom 12 timmar, och modell B ockuperade allt minne innan den ens hunnit ställa upp alla begränsningar. Den bästa metoden var modell C, som löste spelet inom 20 sekunder. Utan kravet att den sista kulan skulle vara i mitten kom den fram till en lösning efter 152 sekunder. Modell C hade alltså fördel av att veta var den sista kulan måste vara. 3. Min maskinella implementation av solitär Förutom att studera tidigare solitärmodeller, har jag själv byggt ett program i programspråket Python som kan lösa solitär. Jag valde att inte spela central solitär, det fanns alltså inget krav på att den sista kulan skulle vara i mitten. Jag har inte optimerat mitt program enligt modellerna i Jefferson m.fl. (2005), utan mitt mål var enbart att bygga ett program som löser problemet. 3.1 Kort beskrivning av programmets uppbyggnad På AIMAs hemsida (Russel & Norvig, 2005) fanns filen search.py, ett kodskelett som jag använde som utgångspunkt för min agent. Jag använde klasserna Node och Problem från filen, modifierade dem en aning så att de passade mitt spel och lade till mina egna funktioner i dem. Jag lånade även två olika sökalgoritmer från filen, nämligen tree_search och breadth_first_tree_search. När man kör programmet med hjälp av någon av sökalgoritmerna och det hittar en lösning så skriver det ut bilder av spelplanen som den ser ut i varje steg av lösningen från slutet till början. På så sätt kan man se hur agenten har valt att lösa problemet och även upptäcka eventuella buggar eftersom man kan se varje drag som agenten gör. Spelplanen ritas upp med hjälp av punkter, mellanrum och stora O:n så att det ser ut som en solitärspelplan. O symboliserar en kula och. Symboliserar ett tomt hål. Nedan visas ett exempel på hur en nod kan se ut då den skrivs ut. O O O O O. O O O O O O O O O O O.. O O O O O. O O O O O O O O 3.2 Klassen Node I detta kapitel presenteras och förklaras de funktioner som finns i klassen Node. Vill du se den fullständiga programkoden, se bilaga 1. Funktionen _init_ skapar nya noder (tillstånd) i spelet. Den tilldelar varje hål på spelplanen antingen värdet True eller värdet False True om det är en kula i hålet och False om det inte är det. Om noden har en förälder, dvs. om man har utgått från en annan nod och kan nå den nya genom göra ett visst drag, så kopierar _init_ föräldern och modifierar den genom att verkligen 8
göra det drag man valt att utföra. Om ingen förälder finns är man i spelets starttillstånd, och i det fallet tilldelas alla hål värdet True utom det mittersta som får värdet False. Funktionen printboard tar en nod som argument och ritar med hjälp av textsträngar upp spelplanen på skärmen. Denna funktion är enbart till för syns skull, kan man säga; den gör att man på ett smidigt sätt kan se hur spelplanen ser ut i varje tillstånd och följa de drag agenten gör. Om man exempelvis anropar printboard med startnoden som argument, får man följande utskrift: O O O O O O O O O O O O O O O O. O O O O O O O O O O O O O O O O Agenten arbetar betydligt snabbare om man inte använder printboard, så i slutändan anropade jag bara denna funktion på den sista noden, när spelet var löst. Under arbetets gång var dock printboard till nytta eftersom den visar om agenten följer de regler som man har tänkt sig (dvs. om man har implementerat reglerna på rätt sätt). Funktionen pegsleft räknar hur många kulor det finns kvar på spelplanen. När det bara finns en kvar har man vunnit! Vidare har vi funktionerna testright, testleft, testup och testdown som utförs på varje hål på spelplanen. Den kollar om det finns en kula i hålet och om den i så fall kan flyttas höger, vänster upp respektive ner. Funktionen legalmoves anropar alla dessa testfunktioner och gör för varje nod en lista över alla tillåtna drag. Funktionerna right, left, up och down utför de drag som agenten väljer att göra (de tidigare presenterade testfunktionerna utför ju inga drag, de kollar bara om de är möjliga att göra). En av dessa funktioner skickas med till _init_ när den nya noden ska skapas. Slutligen har vi funktionen path som skapar en lista av noder från den nod där man befinner sig nu och ända tillbaka till startnoden. Den visar alltså den väg som agenten gått för att komma dit den är just nu och är nödvändig för att agenten ska kunna leta sig tillbaka till en tidigare nod om den gör fel och hamnar i ett tillstånd där spelet är olösbart. Jag använder även denna funktion tillsammans med printboard för att rita upp den färdiga lösningen när spelet är slut. 3.3 Klassen Problem Funktionen _init_ i Problem-klassen tar emot ett nod som argument och skapar ett spel som börjar med den noden (i mitt fall är detta starttillståndet). Funktionen successor tar emot en nod och returnerar en lista över de noder som kan nås från denna, dvs. nodens efterföljare, dess barn. 9
Vidare har vi funktionen goal_test som helt enkelt kollar om måste är uppnått. Är det bara en kula kvar så returnerar funktionen True och skriver ut meddelandet Grattis till vinsten! Är kulan dessutom i mitten av spelplanen så jublar den lite extra. Slutligen finns här funktionen game som anropas för att sätta igång själva spelet. Den anropar helt enkelt en sökalgoritm och börjar leta det är här det spännande börjar! 3.4 Sökalgoritmer På min agent har jag provat sökmetoderna djupet först och bredden först. Dessa funktioner står utanför klasserna i min kod och är i princip kopierade rakt av från AIMAs hemsida, endast någon enstaka variabel är ändrad för att passa min kod. Bredden först-funktionen behövde funktionen Stack och klasserna Queue och FIFOQueue för att kunna fungera, så jag klippte in även dessa rakt av från AIMA. 3.5 Resultat Mitt solitärprogram lyckades lösa spelet på ungefär 25 minuter med hjälp av djupet förstsökning. Jag provade även sökalgoritmen bredden först, men då fick jag efter en stund ett felmeddelande som löd såhär: Traceback (most recent call last): File "Z:\729g11\Projekt\solitaragent.py.py", line 317, in <module> problem1.game(node1) File "Z:\729g11\Projekt\solitaragent.py.py", line 244, in game winning_node=breadth_first_tree_search(problem1) File "Z:\729g11\Projekt\solitaragent.py.py", line 306, in breadth_first_tree_search return tree_search(problem, FIFOQueue()) File "Z:\729g11\Projekt\solitaragent.py.py", line 301, in tree_search fringe.extend(problem.successor(node)) File "Z:\729g11\Projekt\solitaragent.py.py", line 281, in extend self.a.extend(items) MemoryError Jag tolkar MemoryError som att minnesåtgången blev för stor för att kunna lösa problemet, vilket inte är så konstigt när man kör bredden först på ett sökträd med lösningen på djupet 31 och där vissa noder har många barn. Sökträdet blir för stort för att hantera, kanske särskilt eftersom mitt program inte rensar bort de noder som är likadana. Därför valdes djupet först som sökalgoritm. 10
5. Diskussion Jag och Jefferson m.fl. (2005) har inte löst exakt samma problem. Mitt syfte var att bygga ett program som fungerar och löser problemet, medan Jefferson m.fl. (2005) har högre mål och försöker optimera sin lösning så mycket som möjligt. Min lösning är alltså inte lika genomarbetad och välutvecklad, men utgör ändå något intressant att jämföra med. I och med att min agent inte gör någon pruning överhuvudtaget ges insikt i hur pass mycket tid man kan spara på bra pruningmetoder. Jefferson m.fl. (2005) kom med hjälp av sin modell C fram till en lösning på 152 sekunder i varianten utan krav på att kulan ska vara i mitten. Min implementation löste samma problem på 25 minuter, alltså kan man med hjälp av pagodafunktionen och symmetrimetoderna vinna ungefär 22,5 minuter (med en liten felmarginal för att det inte framgår vilken prestanda deras datorer hade). 11
6. Referenslista Tryckta källor Russell, Stuart & Norvig, Peter (2003). Artificial intelligence A modern approach. Pearson Education, Inc. Jefferson, Christopher; Miguel, Angela; Miguel, Ian & Tarin, Armagan (2005) Modelling and solvning English Peg Solitaire. AI Group, Department of Computer Science, University of York, UK. Internet Russell, Stuart & Norvig, Peter (2005). AIMA Python file: search.py [www] <http://aima.cs.berkeley.edu/python/search.html> Hämtat 2010-09-10 12
5 Bilagor 5.1 Bilaga 1 Programkod för solitäragenten # -*- coding: cp1252 -*- import copy import random #from utils import * class Node: """A node in a search tree. Contains a pointer to the parent (the node that this is a successor of) and to the actual state for this node. Note that if a state is arrived at by two paths, then there are two nodes with the same state. Also includes the action that got us to this state, and the total path_cost (also known as g) to reach the node. Other functions may add an f and h value; see best_first_graph_search and astar_search for an explanation of how the f and h values are handled. You will not need to subclass this class.""" def init (self, parent=none, action=none): "Create a search tree Node, derived from a parent by an action." initial_state = [ [None,None,True,True,True,None,None], [None,None,True,True,True,None,None], [True,True,True,True,True,True,True], [True,True,True,False,True,True,True], [True,True,True,True,True,True,True], [None,None,True,True,True,None,None], [None,None,True,True,True,None,None]] self.parent = parent if parent==none: self.board=initial_state else: self.board=copy.deepcopy(parent.board) if action[2]=="right": self.right(action[0], action[1]) if action[2]=="left": self.left(action[0], action[1]) if action[2]=="up": self.up(action[0], action[1]) if action[2]=="down": self.down(action[0], action[1]) def printboard(self): #Visar hur spelplanen ser ut just nu for line in self.board: tecken = "" for hole in line: if hole==true: tecken += "O " elif hole==false: 13
tecken += ". " else: tecken += " " print tecken print "\n" def pegsleft(self): n=0 for line in self.board: for hole in line: if hole==true: n=n+1 return n def testright(self,x,y): """Testar om kulan på plats (x,y) kan flyttas höger""" try: if self.board[x][y] == True and self.board[x][y+1] == True and self.board[x][y+2] == False: return True else: return False except: return None def testleft(self,x,y): """Testar om kulan på plats (x,y) kan flyttas vänster""" try: if self.board[x][y] == True and self.board[x][y-1] == True and self.board[x][y-2] == False and y>=2: return True else: return False except: return None def testup(self,x,y): """Testar om kulan på plats (x,y) kan flyttas upp""" try: if self.board[x][y] == True and self.board[x-1][y] == True and self.board[x-2][y] == False and x>=2: return True else: return False except: return None def testdown(self,x,y): """Testar om kulan på plats (x,y) kan flyttas upp""" try: if self.board[x][y] == True and self.board[x+1][y] == True and self.board[x+2][y] == False: return True else: return False except: 14
return None def legalmoves(self): actions=[] for (x,y) in activeboard: if self.testright(x,y): actions.append([x,y,"right"]) if self.testleft(x,y): actions.append([x,y,"left"]) if self.testup(x,y): actions.append([x,y,"up"]) if self.testdown(x,y): actions.append([x,y,"down"]) return actions def right(self, x,y): #Kulan på position (x,y) hoppar över sin granne till höger self.board[x][y] = False self.board[x][y+1] = False self.board[x][y+2] = True def left(self, x,y): #Kulan på position (x,y) hoppar över sin granne till vänster self.board[x][y] = False self.board[x][y-1] = False self.board[x][y-2] = True def up(self, x,y): #Kulan på position (x,y) hoppar över grannen ovanför self.board[x][y] = False self.board[x-1][y] = False self.board[x-2][y] = True def down(self, x,y): #Gissa vad den här funktionen gör! self.board[x][y] = False self.board[x+1][y] = False self.board[x+2][y] = True def symmetric(self, othernode): tempnode1=copy.deepcopy(self) tempnode2=copy.deepcopy(self) tempnode3=copy.deepcopy(self) tempnode1.board.reverse() for row in tempnode2.board: row.reverse() tempnode3.board.reverse() for row in tempnode3.board: row.reverse() 15
if tempnode1.board == othernode.board: return True elif tempnode2.board == othernode.board: return True elif tempnode3.board == othernode.board: return True else: return False def path(self): "Create a list of nodes from the root to this node." x, result = self, [self] while x.parent: result.append(x.parent) x = x.parent return result class Problem: """The abstract class for a formal problem. You should subclass this and implement the method successor, and possibly init, goal_test, and path_cost. Then you will create instances of your subclass and solve them with the various search functions.""" add def init (self, initial, goal=none): """The constructor specifies the initial state, and possibly a goal state, if there is a unique goal. Your subclass's constructor can other arguments.""" self.initial = initial self.goal = goal def successor(self, state): """Given a state, return a sequence of (action, state) pairs reachable from this state. If there are many successors, consider an iterator that yields the successors one at a time, rather than building them all at once. Iterators will work fine within the framework.""" successors=[] symmetry=false for action in state.legalmoves(): new_state = Node(state, action) successors.append((action, new_state)) return successors the def goal_test(self, state): """Return True if the state is a goal. The default method compares state to self.goal, as specified in the constructor. Implement this method if checking against a single self.goal is not enough.""" if state.pegsleft() == 1 and state.board[3][3]==true: print "Grattis till vinsten! Kulan i mitten och allt! :D" self.goal = True elif state.pegsleft() == 1: print "Grattis till vinsten!" self.goal = True else: self.goal = False 16
return self.goal def game(self, state): for x in range(7): for y in range(7): if (x,y) not in inactiveboard: activeboard.append((x,y)) winning_node=tree_search(problem1, []) for s in winning_node.path(): s.printboard() class Queue: """Queue is an abstract class/interface. There are three types: Stack(): A Last In First Out Queue. FIFOQueue(): A First In First Out Queue. PriorityQueue(lt): Queue where items are sorted by lt, (default <). Each type supports the following methods and functions: q.append(item) -- add an item to the queue q.extend(items) -- equivalent to: for item in items: q.append(item) q.pop() -- return the top item from the queue len(q) -- number of items in q (also q. len()) Note that isinstance(stack(), Queue) is false, because we implement stacks as lists. If Python ever gets interfaces, Queue will be an interface.""" def init (self): abstract def extend(self, items): for item in items: self.append(item) def Stack(): """Return an empty list, suitable as a Last-In-First-Out Queue.""" return [] class FIFOQueue(Queue): """A First-In-First-Out Queue.""" def init (self): self.a = []; self.start = 0 def append(self, item): self.a.append(item) def len (self): return len(self.a) - self.start def extend(self, items): self.a.extend(items) def pop(self): e = self.a[self.start] self.start += 1 if self.start > 5 and self.start > len(self.a)/2: self.a = self.a[self.start:] self.start = 0 return e def tree_search(problem, fringe): """Search through the successors of a problem to find a goal. The argument fringe should be an empty queue. Don't worry about repeated paths to a state. [Fig. 3.8]""" 17
fringe.append((none, Node())) while fringe: node = fringe.pop()[1] if problem.goal_test(node): return node fringe.extend(problem.successor(node)) return None def breadth_first_tree_search(problem): "Search the shallowest nodes in the search tree first. [p 74]" return tree_search(problem, FIFOQueue()) inactiveboard=[(0,0),(0,1),(1,0),(1,1),(0,5),(0,6),(1,5),(1,6),(5,0),(5,1), (6,0),(6,1),(5,5),(5,6),(6,5),(6,6)] activeboard=[] node1=node() problem1 = Problem(node1) problem1.game(node1) 18