1 Artificiell intelligens En agent som spelar Black Jack Andreas Perjons [andpe813] Linköpings Universitet 2019
2 Innehåll Introduktion...3 Metod..4 Programmets komponenter.4 Resultat...5 Diskussion...7 Referenser...8 Appendix.9
Introduktion 3 Black Jack är ett kortspel som grundar sig på probabilitet. Det spelas mot en dealer (casinot) och går ut på att dra kort för att försöka komma så nära (men inte överskrida) 21 som möjligt. I normala fall börjar spelet med en insats från spelaren. Skulle spelare vinna får den tillbaka dubbla insatsen, men förlorar spelare behåller dealern insatsen. Dealern ger spelaren två kort, vars summa motsvarar spelarens poäng. Dealern ger även sig själv ett kort. Klädda kort är dock värda 10 och ess 11, eller 1 om poängen annars skulle överskrida 21. Spelaren får sedan välja om den vill dra ett till kort eller om den vill stanna på de kort som den redan har. Att dra kort kan upprepas så länge spelaren vill, eller tills att dess poäng överskridit 21. Spelaren kan även välja att dubbla, vilket betyder att den bara drar ett kort, men vinner dubbla insatsen om så skulle vara fallet. Om en spelare får två kort av samma valör (ej ess), kan spelaren även välja att splitta, vilket betyder att handen istället spelas som två separata händer. Om detta sker ger dealern de nya händerna varsitt kort till och spelaren får därefter välja att fortsätta dra eller stanna. När spelaren har valt att stanna så drar dealern kort tills att dess poäng blivit, eller överskridit 17. Beroende på lokala regler så kan dealern behöva fortsätta dra kort om dess hand innehåller ett ess (så kallad mjuk 17), att dealern behöver dra på en mjuk 17 minskar husets övertag med ungefär 0,2%. ( Blackjack, 2019, 12 april) Då Black Jack är ett relativt simpelt spel i termerna av vilka drag som kan göras betyder att det är mycket lätt att göra om dem till states som sedan kan användas för att automatisera spelandet. Black Jack spelas i en delvis observerbar miljö, där den information som finns tillgänglig är de kort spelaren har dragit, det kort dealern har dragit och i teorin de kort som finns kvar i leken. I detta Black Jack-spel så används åtta standard-kortlekar och det tordes orimligt att en människa hela tiden skulle kunna hålla koll på vilka kort som finns kvar att dra. En dator kan dock hålla reda på detta, vilket gör att datorn kan använda sig av något som kallas för en Monte Carlo-metod (Towards Data Science, 2018) för att bestämma hur många kort den ska dra och därmed möjliggöra ett automatiserat spelande. Grundtanken i en Monte Carlo-metod är att använda slumpad sampling ett stort antal gånger för att lösa ett i grunden deterministiskt problem ( Deterministic algorithm, 2019, 14 januari). Detta gör att istället för att få en exakt probabilitet så fås en approximering, där ju fler iterationer som körs gör att den approximerade probabiliteten närmar sig den faktiska probabiliteten. Monte Carlo-metoder hanterar slump väldigt bra, vilket gör att de lämpar sig väl att använda i denna situation, då den avgörande faktorn för vinst eller förlust är vilket kort som slumpmässigt dras.
Metod 4 För att genomföra detta projekt var det första som gjordes att programmera ett Black Jack spel från grunden, vilket gjordes i Python 3. Detta gjordes för att underlätta överblicken över variabler samt vetskapen om hur programmet fungerar. Värt att notera här är att det finns hundratals olika variationer av Black Jack ( Blackjack, 2019, 12 april) och de regler och bestämmelser som använts här följer den europeiska standarden i mesta möjliga mån. En kompromiss som dock fick göras var att ingen split-funktion implementerades. Detta berodde på att mängden arbete som skulle behöva läggas ner på att implementera detta inte är proportionerlig mot den inverkan som det skulle ha på programmet. Det intressanta är istället den beslutsprocess som genomförs då agenten ska välja hur många kort den vill dra, vilket kan genomföras utan en split-funktion. En annan förenkling som gjorts är att inget ekonomisystem är implementerat, vilket eliminerar val av insats. Istället så avgör bara programmet om spelaren eller dealern vann. Tillvägagångssättet för beslutsprocessen i detta program är som följande: när agenten fått sina två kort och dealern sitt är det upp till agenten att bestämma om den vill dubbla, fortsätta att dra kort eller stanna. För att avgöra detta kopieras spelets nuvarande state (spelarens kort, dealerns kort, de kort som finns kvar att dra och det val som agenten gjort) och spelas i en simulering 100 gånger, med leken blandad för varje ny gång, där agenten provar att dra olika mängder kort. Detta ger då en approximering av hur många kort som ska dras för att ge den största vinstchansen i det faktiska spelet. Är antalet 0 så stannar agenten, 1 dubblar agenten och 2+ fortsätter agenten att dra kort. Detta kan sen köras i en for-loop för att bestämma hur många faktiska omgångar som ska spelas. I detta fall spelas 100 faktiska omgångar, där antalet vinster blir lika med den procentuella vinstchansen för algoritmen. Programmets komponenter Makedeck Det första som görs i programmet är att en kortlek (bestående av 6 standardkortlekar) genereras. Kortleken är en lista där varje element representerar ett kort och elementen i listan är en tupel som till exempel ser ut som följande: ('Club', 'A'). Kortleken blandas sedan med hjälp av numpy-funktionen np.random.shuffle (The SciPy community, 2019) för att slumpa ordningen. Drawcard
5 Detta är funktionen för att dra ett kort från kortleken och lägga till det till spelarens eller dealerns hand. Detta görs med den inbyggda pop-funktionen där det första elementet i kortleken tas bort och sedan läggs till i endera spelarens eller dealerns hand (som också är listor). Getscore Detta är funktionen för att beräkna en hands poäng. Funktionen tar in en hand (lista) som parameter och går igenom den elementvis med hjälp av en for-loop som summerar och slutligen returnerar poängen. Choicesequence I denna funktion så utförs handlingar beroende på om kort ska dras eller ej. Detta har blivit bestämt i funktionen gameinstance. Gameinstance Denna funktion tar en kopia (deepcopy) (Python Software Foundation, 2019) av spelets nuvarande state, testar att dra olika många kort och utvärderar resultatet för vilken den approximerade optimala mängden kort är att dra. Actionsdone När agenten är klar med sina val körs denna funktion, den lägger automatiskt till kort till dealerns hand via Drawcard tills att summan av dealerns hand blivit, eller överstiger 17. När detta är klart jämförs sedan spelarens hand med dealerns för att avgöra vem som vann. Resultat Programmets resultat värderas genom hur stor procent av spelen som vanns. Nedan följer några exempelkörningar av programmet. Värt att notera är att resultaten varierar rejält mellan körningarna (vinstprocent varierar mellan ungefär 35 59%), men att resultatet vid de flesta körningarna är större än om slumpmässiga val görs (vinstprocent varierar då mellan ungefär 20 35%).
6 Figur 1: Sammansättning av 5 separata körningar med slumpmässiga val. Figur 2: Sammansättning av 5 separata körningar med Monte Carlo-metod
Diskussion 7 Användning av Monte Carlo-metod för att automatisera Black Jack-spelande har visat sig vara ett effektivt tillvägagångssätt. Det var inte från början tänkt att agenten skulle ha en genomsnittligt positiv vinstprocent, utan istället hur nära en genomsnittlig positiv vinstprocent den kan komma med tanke på de förutsättningar som hafts. Även om agenten för det mesta har en vinstchans som ligger under 50% så påvisar den i alla fall att beslutsprocessen oftast är bättre än att göra slumpmässiga val. En bidragande faktor till den vinstchans som fåtts fram är att Black Jack i grunden är designat så att dealern (casinot) alltid har ett övertag. Detta gör att ju närmare antalet rundor som spelas är till oändligheten, ju närmare blir den approximerade vinstchansen den faktiska vinstchansen som då är under 50%. En ytterligare bidragande faktor till vinstchansen som fås är saknaden av split-funktionen i programmet, då möjligheten att splitta endast är till spelarens fördel. Nämnvärt är att agenten inte heller använder sig av någon allmän strategi när den fattar beslut (lookup-table för alla möjliga kombinationer av kort), vilket om använt som heuristik skulle kunna tänkas förbättra agentens vinstchans. Vad som skulle vara intressant att vidareutveckla i detta projekt är en split-funktion, vilket dock kräver att den grundläggande kodningen för hur spelarens hand (händer) hanteras skrivs om. Ett ekonomisystem skulle även kunna implementeras där agenten själv får bestämma insats, vilket då integrerar en ytterligare form av beslutsfattande. Något som skulle vara intressant att undersöka är hur väl agenten presterar i jämförelse med en människa som spelar. I detta fall så behöver inte agenten spela perfekt, utan bara så länge agenten presterar bättre än människan gör att det finns belägg för det som vi skulle kalla för artificiell intelligens.
Referenser 8 Blackjack (2019, 12 april) I Wikipedia. Hämtad 2019-04-15 från https://en.wikipedia.org/wiki/blackjack Determinsitic algorithm (2019, 14 januari) I Wikipedia. Hämtad 2019-04-15 från https://en.wikipedia.org/wiki/deterministic_algorithm Python Software Foundation (2019). Shallow and deep copy operations. Hämtad 2019-04-15 från https://docs.python.org/2/library/copy.html The SciPy community (2019). numpy.random.shuffle. Hämtad 2019-04-15 från https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.shuffle.html Towards Data Science (2018). An Overview of Monte Carlo Methods. Hämtad 2019-04-15 från https://towardsdatascience.com/an-overview-of-monte-carlo-methods- 675384eb1694?fbclid=IwAR0SQxI9AvmXaTjL6bnKr1v06E_xAHZWmYpqNdFEsrKcYWj HmnuHw65gWsc
Appendix 9 Kod import numpy as np import os import copy from collections import Counter def choicesequence(deck, hand, dealerhand, choice): """Make an action. Parameters: deck (list) - List with the current cards in the deck. hand (list) - List of tuples representing the player's cards. dealerhand (list) - List of tuples representing the dealer's cards. choice (string) - Which choice of action that has been made. Returns: A list containing the player's hand and which choice of action that has been made. """ statedeck = copy.deepcopy(deck) statehand = copy.deepcopy(hand)
10 statedealerhand = copy.deepcopy(dealerhand) statechoice = copy.deepcopy(choice) state = [statedeck, statehand, statedealerhand, statechoice] action = gameinstance(state) if action == 'DD': drawcard(deck, hand, 1) choice = 'DD' return [hand, choice] elif action == 'DC': drawcard(deck, hand, 1) choice = 'DC' return [hand, choice] elif action == 'NA': choice = 'NA' return [hand, choice] def gameinstance(state): """Play 50 simulations of the current actual hand. Determine which action is most likely to result in a win. Parameters: state (list) - A copy of the current actual game (hands, deck, choice).
11 Returns: bestaction (string) - The best action to take in the current state. """ idrawcounter = Counter() for cards in range(0, 21 - getscore(copy.deepcopy(state[1]))): for games in range(0, 50): ideck = copy.deepcopy(state[0]) np.random.shuffle(ideck) iplayerhand = copy.deepcopy(state[1]) idealerhand = copy.deepcopy(state[2]) ichoice = copy.deepcopy(state[3]) if cards > 0: drawcard(ideck, iplayerhand, cards) ichoice) iresult = actionsdone(ideck, iplayerhand, idealerhand, if iresult == 'W': idrawcounter[cards] += 1 if not idrawcounter: bestaction = 'NA' elif int(idrawcounter.most_common(1)[0][0]) == 0: bestaction = 'NA' elif int(idrawcounter.most_common(1)[0][0]) == 1: bestaction = 'DD' else:
12 bestaction = 'DC' return bestaction def drawcard(deck, recipient, howmany): """Pop a card from deck to hand. Parameters: deck (list) - List with the current cards in the deck. recipient (list) - The hand that recieves the card. Returns: recipient (list) - The hand that recieved the card. """ for card in range(0, howmany): if len(deck) == 0: deck = makedeck() np.random.shuffle(deck) drawncard = deck.pop(0) recipient.append(drawncard) return recipient def getscore(hand): """Determine the current score of a hand. Parameters: hand (list) - The hand of which to determine the score.
13 Returns: score (integer) - The score of the hand. """ score = 0 aces = 0 for card in hand: if card[1] == 'J' or card[1] == 'Q' or card[1] == 'K': score += 10 elif card[1] == 'A': score += 11 aces += 1 else: score += card[1] if aces > 0 and score > 21: score -= 10 aces -= 1 return score def makedeck(): """Return a list representing six decks""" deck = [] for deckcounter in range(0, 6): for counter1to53 in range(1, 53):
14 if counter1to53 < 14: if counter1to53 == 1: deck.append(('spade', 'A')) elif counter1to53 == 11: deck.append(('spade', 'J')) elif counter1to53 == 12: deck.append(('spade', 'Q')) elif counter1to53 == 13: deck.append(('spade', 'K')) else: deck.append(('spade ', counter1to53)) elif counter1to53 > 13 and counter1to53 < 27: if counter1to53-13 == 1: deck.append(('heart', 'A')) elif counter1to53-13 == 11: deck.append(('heart', 'J')) elif counter1to53-13 == 12: deck.append(('heart', 'Q')) elif counter1to53-13 == 13: deck.append(('heart', 'K')) else: deck.append(('heart ', counter1to53-13)) elif counter1to53 > 26 and counter1to53 < 40: if counter1to53-26 == 1:
15 deck.append(('club', 'A')) elif counter1to53-26 == 11: deck.append(('club', 'J')) elif counter1to53-26 == 12: deck.append(('club', 'Q')) elif counter1to53-26 == 13: deck.append(('club', 'K')) else: deck.append(('club ', counter1to53-26)) else: if counter1to53-39 == 1: deck.append(('diamond', 'A')) elif counter1to53-39 == 11: deck.append(('diamond', 'J')) elif counter1to53-39 == 12: deck.append(('diamond', 'Q')) elif counter1to53-39 == 13: deck.append(('diamond', 'K')) else: deck.append(('diamond ', counter1to53-39)) return deck def actionsdone(deck, playerhand, dealerhand, choice):
16 """Dealer draws and the game ends. Parameters: deck (list) - List with the current cards in the deck. hand (list) - List of tuples representing the player's cards. dealerhand (list) - List of tuples representing the dealer's cards. choice (string) - Which choice of action that has been made. Returns - A character (W - win, L - loss, D - draw) to indicate who won. """ gameloss = False if getscore(playerhand) > 21: gameloss = True else: while getscore(dealerhand) < 17: drawcard(deck, dealerhand, 1) os.system('clear') print('your hand: ', playerhand, getscore(playerhand)) print('dealers hand: ', dealerhand, getscore(dealerhand)) if getscore(playerhand) > getscore(dealerhand) and getscore(playerhand) < 22 and not gameloss: return 'W' elif getscore(playerhand) == getscore(dealerhand) and getscore(playerhand) < 22 and not gameloss:
17 return 'D' elif getscore(dealerhand) > 21 and getscore(playerhand) < 22: return 'W' else: return 'L' def main(): deck = [] deck = makedeck() np.random.shuffle(deck) wins = 0 losses = 0 draws = 0 for games in range(0, 100): playerhand = [] dealerhand = [] cardsdrawn = 0 gameloss = False acesinhand = 0 choice = 'ST' os.system('clear') while choice!= 'NA': if len(playerhand) < 2:
18 drawcard(deck, playerhand, 2) if len(dealerhand) < 1: drawcard(deck, dealerhand, 1) choicetuple = choicesequence(deck, playerhand, dealerhand, choice) playerhand = choicetuple[0] choice = choicetuple[1] if getscore(playerhand) > 22 or choice == 'DD': break result = actionsdone(deck, playerhand, dealerhand, choice) if result == 'W': wins += 1 elif result == 'L': losses += 1 elif result == 'D': draws += 1 print('wins: ', wins, 'Losses: ', losses, 'Draws: ', draws) print('win percentage: ', (wins/(wins + losses + draws)) * 100, '%') if name == " main ": main()