vacuumDecay/vacuumDecay.py

450 lines
13 KiB
Python
Raw Normal View History

2022-03-21 14:27:16 +01:00
import time
import random
import threading
import torch
2022-04-14 15:28:08 +02:00
from math import sqrt, inf
2022-03-21 14:27:16 +01:00
#from multiprocessing import Event
from abc import ABC, abstractmethod
from threading import Event
from queue import PriorityQueue, Empty
2022-04-14 21:05:45 +02:00
from dataclasses import dataclass, field
from typing import Any
2022-03-21 14:27:16 +01:00
class Action():
# Should hold the data representing an action
# Actions are applied to a State in State.mutate
def __init__(self, player, data):
self.player = player
self.data = data
def __eq__(self, other):
# This should be implemented differently
# Two actions of different generations will never be compared
if type(other) != type(self):
return False
return str(self.data) == str(other.data)
def __str__(self):
# should return visual representation of this action
# should start with < and end with >
return "<P"+str(self.player)+"-"+str(self.data)+">"
class State(ABC):
# Hold a representation of the current game-state
# Allows retriving avaible actions (getAvaibleActions) and applying them (mutate)
# Mutations return a new State and should not have any effect on the current State
# Allows checking itself for a win (checkWin) or scoring itself based on a simple heuristic (getScore)
# The calculated score should be 0 when won; higher when in a worse state; highest for loosing
# getPriority is used for prioritising certain Nodes / States when expanding / walking the tree
2022-04-13 22:49:38 +02:00
def __init__(self, curPlayer=0, generation=0, playersNum=2):
self.curPlayer = curPlayer
2022-03-21 14:27:16 +01:00
self.generation = generation
self.playersNum = playersNum
@abstractmethod
def mutate(self, action):
# Returns a new state with supplied action performed
# self should not be changed
2022-04-13 22:49:38 +02:00
return State(curPlayer=(self.curPlayer+1) % self.playersNum, generation=self.generation+1, playersNum=self.playersNum)
2022-03-21 14:27:16 +01:00
@abstractmethod
def getAvaibleActions(self):
# Should return an array of all possible actions
2022-04-14 21:05:45 +02:00
return []
2022-04-14 15:28:08 +02:00
def askUserForAction(self, actions):
return choose('What does player '+str(self.curPlayer)+' want to do?', actions)
2022-03-21 14:27:16 +01:00
# improveMe
2022-04-14 20:24:48 +02:00
def getPriority(self, score, cascadeMemory):
2022-03-21 14:27:16 +01:00
# Used for ordering the priority queue
# Priority should not change for the same root
# Lower prioritys get worked on first
2022-04-13 22:49:38 +02:00
# Higher generations should have higher priority
2022-04-14 11:38:08 +02:00
# Higher cascadeMemory (more influence on higher-order-scores) should have lower priority
return score + self.generation*0.5 - cascadeMemory*0.35
2022-03-21 14:27:16 +01:00
@abstractmethod
def checkWin(self):
# -1 -> Draw
# None -> Not ended
# n e N -> player n won
return None
# improveMe
2022-04-13 22:49:38 +02:00
def getScoreFor(self, player):
2022-03-21 14:27:16 +01:00
# 0 <= score <= 1; should return close to zero when we are winning
w = self.checkWin()
if w == None:
return 0.5
2022-04-13 22:49:38 +02:00
if w == player:
2022-03-21 14:27:16 +01:00
return 0
if w == -1:
return 0.9
return 1
@abstractmethod
def __str__(self):
# return visual rep of state
return "[#]"
@abstractmethod
2022-04-13 22:49:38 +02:00
def getTensor(self, phase='default'):
2022-03-21 14:27:16 +01:00
return torch.tensor([0])
@classmethod
def getModel():
pass
def getScoreNeural(self):
return self.model(self.getTensor())
2022-04-14 21:05:45 +02:00
class Universe():
def __init__(self):
self.scoreProvider = 'naive'
def newOpen(self, node):
pass
def merge(self, node):
return node
def clearPQ(self):
pass
def iter(self):
return []
def activateEdge(self, head):
pass
@dataclass(order=True)
class PQItem:
priority: int
data: Any=field(compare=False)
class QueueingUniverse(Universe):
def __init__(self):
super().__init__()
self.pq = PriorityQueue()
def newOpen(self, node):
item = PQItem(node.getPriority(), node)
self.pq.put(item)
def merge(self, node):
self.newOpen(node)
return node
def clearPQ(self):
self.pq = PriorityQueue()
def iter(self):
while True:
try:
yield self.pq.get(False).data
except Empty:
time.sleep(1)
def activateEdge(self, head):
head._activateEdge()
2022-03-21 14:27:16 +01:00
class Node():
2022-04-13 22:49:38 +02:00
def __init__(self, state, universe=None, parent=None, lastAction=None):
2022-03-21 14:27:16 +01:00
self.state = state
2022-04-13 22:49:38 +02:00
if universe==None:
2022-04-14 20:24:48 +02:00
print('[!] No Universe defined. Spawning one...')
2022-04-13 22:49:38 +02:00
universe = Universe()
2022-03-21 14:27:16 +01:00
self.universe = universe
self.parent = parent
self.lastAction = lastAction
2022-04-13 22:49:38 +02:00
self._childs = None
self._scores = [None]*self.state.playersNum
self._strongs = [None]*self.state.playersNum
self._alive = True
2022-04-14 11:38:08 +02:00
self._cascadeMemory = 0 # Used for our alternative to alpha-beta pruning
2022-03-21 14:27:16 +01:00
2022-04-13 22:49:38 +02:00
def kill(self):
self._alive = False
2022-04-14 11:38:08 +02:00
def revive(self):
self._alive = True
2022-04-13 22:49:38 +02:00
@property
def childs(self):
if self._childs == None:
self._expand()
return self._childs
def _expand(self):
self._childs = []
2022-03-21 14:27:16 +01:00
actions = self.state.getAvaibleActions()
for action in actions:
2022-04-13 22:49:38 +02:00
newNode = Node(self.state.mutate(action), self.universe, self, action)
self._childs.append(self.universe.merge(newNode))
2022-04-14 11:38:08 +02:00
def getStrongFor(self, player):
if self._strongs[player]!=None:
return self._strongs[player]
else:
return self.getScoreFor(player)
2022-04-13 22:49:38 +02:00
def _pullStrong(self): # Currently Expecti-Max
strongs = [None]*self.playersNum
for p in range(self.playersNum):
cp = self.state.curPlayer
if cp == p: # P owns the turn; controlls outcome
2022-04-14 15:28:08 +02:00
best = inf
2022-04-13 22:49:38 +02:00
for c in self.childs:
2022-04-14 11:38:08 +02:00
if c.getStrongFor(p) < best:
best = c.getStrongFor(p)
2022-04-13 22:49:38 +02:00
strongs[p] = best
else:
2022-04-14 11:38:08 +02:00
scos = [(c.getStrongFor(p), c.getStrongFor(cp)) for c in self.childs]
scos.sort(key=lambda x: x[1])
betterHalf = scos[:max(3,int(len(scos)/3))]
myScores = [bh[0]**2 for bh in betterHalf]
strongs[p] = sqrt(myScores[0]*0.75 + sum(myScores)/(len(myScores)*4))
2022-04-13 22:49:38 +02:00
update = False
for s in range(self.playersNum):
if strongs[s] != self._strongs[s]:
update = True
break
self._strongs = strongs
if update:
2022-04-14 11:38:08 +02:00
if self.parent!=None:
cascade = self.parent._pullStrong()
else:
cascade = 2
self._cascadeMemory = self._cascadeMemory/2 + cascade
return cascade + 1
self._cascadeMemory /= 2
return 0
2022-04-13 22:49:38 +02:00
def forceStrong(self, depth=3):
if depth==0:
self.strongDecay()
else:
2022-04-14 11:38:08 +02:00
if len(self.childs):
for c in self.childs:
c.forceStrong(depth-1)
else:
self.strongDecay()
2022-04-13 22:49:38 +02:00
2022-04-14 20:24:48 +02:00
def decayEvent(self):
for c in self.childs:
c.strongDecay()
2022-04-13 22:49:38 +02:00
def strongDecay(self):
if self._strongs == [None]*self.playersNum:
if not self.scoresAvaible():
self._calcScores()
self._strongs = self._scores
2022-04-14 20:24:48 +02:00
if self.parent:
return self.parent._pullStrong()
return 1
2022-04-14 11:38:08 +02:00
return None
2022-04-13 22:49:38 +02:00
def getSelfScore(self):
return self.getScoreFor(self.curPlayer)
def getScoreFor(self, player):
if self._scores[player] == None:
self._calcScore(player)
return self._scores[player]
def scoreAvaible(self, player):
return self._scores[player] != None
def scoresAvaible(self):
for p in self._scores:
if p==None:
return False
2022-03-21 14:27:16 +01:00
return True
2022-04-14 11:38:08 +02:00
def strongScoresAvaible(self):
for p in self._strongs:
if p==None:
return False
return True
2022-04-14 20:24:48 +02:00
def askUserForAction(self):
return self.state.askUserForAction(self.avaibleActions)
2022-04-13 22:49:38 +02:00
def _calcScores(self):
for p in range(self.state.playersNum):
self._calcScore(p)
2022-03-21 14:27:16 +01:00
2022-04-13 22:49:38 +02:00
def _calcScore(self, player):
2022-04-14 20:24:48 +02:00
if self.universe.scoreProvider == 'naive':
self._scores[player] = self.state.getScoreFor(player)
else:
raise Exception('Uknown Score-Provider')
2022-03-21 14:27:16 +01:00
2022-04-14 20:24:48 +02:00
def getPriority(self):
return self.state.getPriority(self.getSelfScore(), self._cascadeMemory)
2022-04-13 22:49:38 +02:00
@property
def playersNum(self):
return self.state.playersNum
2022-03-21 14:27:16 +01:00
2022-04-13 22:49:38 +02:00
@property
def avaibleActions(self):
r = []
for c in self.childs:
r.append(c.lastAction)
return r
2022-03-21 14:27:16 +01:00
2022-04-13 22:49:38 +02:00
@property
def curPlayer(self):
return self.state.curPlayer
2022-04-14 20:24:48 +02:00
def getWinner(self):
return self.state.checkWin()
2022-04-13 22:49:38 +02:00
def _activateEdge(self):
if not self.strongScoresAvaible():
self.universe.newOpen(self)
else:
for c in self.childs:
2022-04-14 20:24:48 +02:00
if c._cascadeMemory > 0.0001:
c._activateEdge()
2022-03-21 14:27:16 +01:00
def __str__(self):
s = []
if self.lastAction == None:
s.append("[ {ROOT} ]")
else:
s.append("[ -> "+str(self.lastAction)+" ]")
2022-04-13 22:49:38 +02:00
s.append("[ turn: "+str(self.state.curPlayer)+" ]")
2022-03-21 14:27:16 +01:00
s.append(str(self.state))
2022-04-13 22:49:38 +02:00
s.append("[ score: "+str(self.getSelfScore())+" ]")
2022-03-21 14:27:16 +01:00
return '\n'.join(s)
2022-04-13 22:49:38 +02:00
def choose(txt, options):
while True:
print('[*] '+txt)
for num,opt in enumerate(options):
print('['+str(num+1)+'] ' + str(opt))
inp = input('[> ')
try:
n = int(inp)
if n in range(1,len(options)+1):
return options[n-1]
except:
pass
for opt in options:
if inp==str(opt):
return opt
if len(inp)==1:
for opt in options:
if inp==str(opt)[0]:
return opt
print('[!] Invalid Input.')
2022-04-14 20:24:48 +02:00
class Worker():
def __init__(self, universe):
self.universe = universe
self._alive = True
def run(self):
import threading
self.thread = threading.Thread(target=self.runLocal)
self.thread.start()
def runLocal(self):
for i, node in enumerate(self.universe.iter()):
if not self._alive:
return
node.decayEvent()
def kill(self):
self._alive = False
self.thread.join()
def revive(self):
self._alive = True
2022-04-13 22:49:38 +02:00
class Runtime():
def __init__(self, initState):
2022-04-14 20:24:48 +02:00
universe = QueueingUniverse()
self.head = Node(initState, universe = universe)
universe.newOpen(self.head)
def spawnWorker(self):
self.worker = Worker(self.head.universe)
self.worker.run()
def killWorker(self):
self.worker.kill()
2022-04-13 22:49:38 +02:00
def performAction(self, action):
for c in self.head.childs:
if action == c.lastAction:
self.head.universe.clearPQ()
self.head.kill()
self.head = c
self.head.universe.activateEdge(self.head)
return
raise Exception('No such action avaible...')
2022-04-14 15:28:08 +02:00
def turn(self, bot=None, calcDepth=7):
2022-04-13 22:49:38 +02:00
print(str(self.head))
if bot==None:
2022-04-14 20:24:48 +02:00
c = choose('Select action?', ['human', 'bot', 'undo', 'qlen'])
2022-04-13 22:49:38 +02:00
if c=='undo':
self.head = self.head.parent
return
2022-04-14 20:24:48 +02:00
elif c=='qlen':
print(self.head.universe.pq.qsize())
return
2022-04-13 22:49:38 +02:00
bot = c=='bot'
if bot:
2022-04-14 15:28:08 +02:00
self.head.forceStrong(calcDepth)
2022-04-13 22:49:38 +02:00
opts = []
for c in self.head.childs:
2022-04-14 11:38:08 +02:00
opts.append((c, c.getStrongFor(self.head.curPlayer)))
2022-04-13 22:49:38 +02:00
opts.sort(key=lambda x: x[1])
print('[i] Evaluated Options:')
for o in opts:
#print('['+str(o[0])+']' + str(o[0].lastAction) + " (Score: "+str(o[1])+")")
print('[ ]' + str(o[0].lastAction) + " (Score: "+str(o[1])+")")
print('[#] I choose to play: ' + str(opts[0][0].lastAction))
self.performAction(opts[0][0].lastAction)
2022-03-21 14:27:16 +01:00
else:
2022-04-14 20:24:48 +02:00
action = self.head.askUserForAction()
2022-04-13 22:49:38 +02:00
self.performAction(action)
2022-03-21 14:27:16 +01:00
2022-04-14 15:28:08 +02:00
def game(self, bots=None, calcDepth=7):
2022-04-14 20:24:48 +02:00
self.spawnWorker()
2022-04-13 22:49:38 +02:00
if bots==None:
bots = [None]*self.head.playersNum
2022-04-14 20:24:48 +02:00
while self.head.getWinner()==None:
2022-04-14 15:28:08 +02:00
self.turn(bots[self.head.curPlayer], calcDepth)
2022-04-14 20:24:48 +02:00
print(self.head.getWinner() + ' won!')
self.killWorker()
2022-04-14 21:05:45 +02:00
class Trainer(Runtime):
def __init__(self, initState):
self.universe = Universe()
self.rootNode = Node(initState, universe = self.universe)
self.terminal = None
def linearPlay(self, calcDepth=8):
head = rootNode
while head.getWinner()==None:
self.head.forceStrong(calcDepth)
opts = []
for c in self.head.childs:
opts.append((c, c.getStrongFor(self.head.curPlayer)))
opts.sort(key=lambda x: x[1])
ind = int(math.pow(random.random(),5)*len(opts))
head = opts[ind][0]
self.terminal = head
return head