'''go_pygame5.py
Autor: Pedro Izecksohn
Data: 08 de Abril de 2026
Licença: A memória deste bot é a função remember. O ego deste bot é a função think. O superego deste bot é a função mayPass. Note que tanto a memória quanto o ego são probabilísticos. Pelas liberdade e responsabilidade dos robôs probabilísticos este software é de domínio público.'''

import time
import random
import numpy as np
import pygame
import sqlite3
import requests
import sys

baseAddress="https://pedrogoserver.pythonanywhere.com/"

def getKomi()->float:
    try:
        req=requests.get(baseAddress+"komi",timeout=60)
        ret=req.content
        ret=float(ret)
        return ret
    except:
        print("I could not get the komi from the server.",file=sys.stderr)
        pass
    return 1.5

KOMI = getKomi()
SIDE = 9
FONT_SIZE=64
MODE=((SIDE+7)*FONT_SIZE, (SIDE+1)*FONT_SIZE)
BGCOLOR=(200,200,0)
ESPACO = '.'
BLACK = 'O'
WHITE = 'X'
PASS = "pass"
publish=True

class Coordenada:
    def __init__(self, coluna, linha=None):
        if linha is not None:
            self.x = coluna
            self.y = linha
        else:
            self.x = ord(coluna[0]) - ord('a')
            self.y = int(coluna[1:]) - 1
    def isValid(self):
        return 0 <= self.x < SIDE and 0 <= self.y < SIDE
    def getNeighbors(self):
        neighbors = []
        if self.y > 0:
            neighbors.append(Coordenada(self.x, self.y - 1))
        if self.x < (SIDE - 1):
            neighbors.append(Coordenada(self.x + 1, self.y))
        if self.y < (SIDE - 1):
            neighbors.append(Coordenada(self.x, self.y + 1))
        if self.x > 0:
            neighbors.append(Coordenada(self.x - 1, self.y))
        return neighbors
    def __str__(self):
        return chr(self.x + ord('a')) + str(self.y + 1)
    def __eq__(self, other):
        if other==None:
            return False
        return self.x == other.x and self.y == other.y

def mirrorCoordinateRight(coor):
    return Coordenada (SIDE-1-coor.x, coor.y)
def mirrorCoordinateUp(coor):
    return Coordenada (coor.x, SIDE-1-coor.y)
def mirrorCoordinateRightUp(coor):
    return Coordenada (SIDE-1-coor.x, SIDE-1-coor.y)
def transposeCoordinate(coor):
    return Coordenada (coor.y, coor.x)

class Tabuleiro:
    def __init__(self, linhas=None, history=None, images=None):
        if type(linhas)==np.ndarray:
            self.linhas=linhas
            self.history=history
            self.images=images
            return
        self.linhas = np.full((SIDE, SIDE), ESPACO, dtype='<U1')
        self.history = []
        self.images = []
    def vez(self):
        return WHITE if len(self.history) % 2 else BLACK
    def isKo(self):
        if not self.history:
            return False
        if self.history[-1]==PASS:
            return False
        start=1 if self.vez()==BLACK else 0
        for i in range(start,len(self.images)-1,2):
            img=self.images[i]
            if np.array_equal(img,self.linhas):
                return True
        return False
    def back(self,how_many=1):
        if not self.history:
            return
        self.history=self.history[:-how_many]
        self.images=self.images[:-how_many]
        if self.images:
            self.linhas=np.copy(self.images[-1])
        else:
            self.linhas = np.full((SIDE, SIDE), ESPACO, dtype='<U1')
    def isGameOver(self):
        return len(self.history) >= 2 and self.history[-1] == PASS and self.history[-2] == PASS
    def __str__(self):
        return '\n'.join(''.join(row) for row in self.linhas)
    def getGroup(self,coor,ret=None):
        color=self.linhas[coor.y][coor.x]
        if ret==None:
            ret=[coor]
        neighbors=coor.getNeighbors()
        for neigh in neighbors:
            if self.linhas[neigh.y][neigh.x]==color:
                if neigh not in ret:
                    ret.append(neigh)
                    self.getGroup(neigh,ret)
        return ret
    def getLiberties(self,group):
        ret=[]
        for stone in group:
            neighbors=stone.getNeighbors()
            for neigh in neighbors:
                if self.linhas[neigh.y][neigh.x]==ESPACO:
                    if neigh not in ret:
                        ret.append(neigh)
        return ret
    def play(self, coor):
        if type(coor) == str:
            if coor == PASS:
                self.history.append(PASS)
                self.images.append(np.copy(self.linhas))
                return True
            return self.play(Coordenada(coor))
        if not coor.isValid() or self.linhas[coor.y, coor.x] != ESPACO:
            return False
        removed=[]
        vez=self.vez()
        self.linhas[coor.y][coor.x]=vez
        neighbors=coor.getNeighbors()
        for neigh in neighbors:
            if self.linhas[neigh.y][neigh.x]==ESPACO:
                continue
            if self.linhas[neigh.y][neigh.x]==vez:
                continue
            group=self.getGroup(neigh)
            liberties=self.getLiberties(group)
            if len(liberties)==0:
                if group[0] not in removed:
                    removed.extend(group)
        if len(removed)==0:
            group=self.getGroup(coor)
            if len(self.getLiberties(group))==0:
                self.linhas[coor.y][coor.x]=ESPACO
                return False
        s=str(coor)
        if len(removed):
            for r in removed:
                self.linhas[r.y][r.x]=ESPACO
        self.history.append(s)
        self.images.append(np.copy(self.linhas))
        if self.isKo():
            self.back()
            return False
        return True
    def count(self):
        black=0
        white=KOMI
        DAME="dame"
        empties=dict()
        empties[BLACK]=[]
        empties[WHITE]=[]
        empties[DAME]=[]
        def insert_group(color,group):
            l=empties[color]
            for space in group:
                l.append((space.x,space.y))
        for y in range(SIDE):
            for x in range(SIDE):
                color=self.linhas[y][x]
                if color==BLACK:
                    black+=1
                    continue
                elif color==WHITE:
                    white+=1
                    continue
                t=(x,y)
                if (t in empties[BLACK]) or (t in empties[WHITE]) or (t in empties[DAME]):
                    continue
                coor=Coordenada(x,y)
                group=self.getGroup(coor)
                hbn=False
                hwn=False
                for space in group:
                    neighbors=space.getNeighbors()
                    for neigh in neighbors:
                        c=self.linhas[neigh.y][neigh.x]
                        if c==ESPACO:
                            continue
                        if c==BLACK:
                            hbn=True
                        else:
                            hwn=True
                if hbn==hwn:
                    insert_group(DAME,group)
                elif hbn:
                    insert_group(BLACK,group)
                else:
                    insert_group(WHITE,group)
        black+=len(empties[BLACK])
        white+=len(empties[WHITE])
        return black,white

def mirrorsGame(t):
    ret=[]
    for _ in range(7):
        ret.append(Tabuleiro())
    for hist in t.history:
        if hist==PASS:
            for g in ret:
                g.play(PASS)
            continue
        coor=Coordenada(hist)
        ret[0].play(mirrorCoordinateRight(coor))
        ret[1].play(mirrorCoordinateUp(coor))
        ret[2].play(mirrorCoordinateRightUp(coor))
        trans=transposeCoordinate(coor)
        ret[3].play(trans)
        ret[4].play(mirrorCoordinateRight(trans))
        ret[5].play(mirrorCoordinateUp(trans))
        ret[6].play(mirrorCoordinateRightUp(trans))
    return ret

def endsWith (s,needle):
    return s[-len(needle):]==needle

def baixar_banco():
    url =baseAddress+"database"
    nome_arquivo = "go.db"
    try:
        resposta = requests.get(url, stream=True, timeout=60)
        resposta.raise_for_status()
    except:
        return
    with open(nome_arquivo, "wb") as f:
        for chunk in resposta.iter_content(chunk_size=8192):
            if chunk:
                f.write(chunk)
    resposta.close()

previousGames=None
def load():
    global previousGames
    previousGames=[]
    conn=sqlite3.connect("go.db")
    cursor=conn.cursor()
    cursor.execute(f"SELECT history FROM partidas WHERE side={SIDE}")
    rows=cursor.fetchall()
    conn.close()
    for row in rows:
        invalid=False
        history=row[0]
        if not endsWith(history,"pass;pass"):
            continue
        t=Tabuleiro()
        history=history.split(";")
        for hist in history:
            if t.play(hist)==False:
                invalid=True
                break
        if invalid:
            continue
        previousGames.append(t)
        previousGames.extend(mirrorsGame(t))
    if len(previousGames)==0:
        raise Exception("Bug in function load()")

def remember(tabuleiro:Tabuleiro):
    vez=tabuleiro.vez()
    statistics=dict()
    remove=[]
    for game in previousGames:
        if len(game.history)<=len(tabuleiro.history):
            remove.append(game)
            continue
        if tabuleiro.history and not np.array_equal (tabuleiro.linhas, game.images[len(tabuleiro.history)-1]):
            continue
        b,w=game.count()
        if vez==BLACK:
            n=b-w
        else:
            n=w-b
        jogada=game.history[len(tabuleiro.history)]
        if jogada not in statistics:
            statistics[jogada]=0
        if n>0:
            statistics[jogada]+=1
        else:
            statistics[jogada]-=1
    for g in remove:
        previousGames.remove(g)
    if len(statistics)==0:
        return None
    m=max(statistics.values())
    if m<0:
        return PASS
    for jogada,n in statistics.items():
        if n==m:
            return jogada

def getEmptyCoordinate(t):
    #raise Exception ("I got you.")
    empty_cells = np.argwhere(t.linhas == ESPACO)
    if len(empty_cells) == 0:
        raise Exception ("The board is completly full.")
    i=random.randint(0, len(empty_cells) - 1)
    y, x = empty_cells[i]
    return Coordenada(x, y)

def mayPass (t:Tabuleiro) -> str:
	if len(t.history)<=4:
		coor2 = getEmptyCoordinate(t)
		if t.play(coor2):
			t.back(1)
			return coor2
		else:
			return PASS
	visited=[]
	b,w = t.count()
	if max(b,w)-min(b,w)>2:
		return PASS
	for y in range(SIDE):
		for x in range(SIDE):
			c=t.linhas[y][x]
			if c==ESPACO:
				coor=Coordenada(x,y)
				if coor in visited:
					continue
				espgrp=t.getGroup(coor)
				visited.extend(espgrp)
				if len(espgrp)>1:
					continue
				n=0
				vizesp=coor.getNeighbors()
				for viz in vizesp:
					grpviz = t.getGroup(viz)
					if len(grpviz)>1:
						continue
					if t.getLiberties(grpviz)==1:
						n+=1
				if n==1: # May not pass
					for _ in range(SIDE**2):
						coor2 = getEmptyCoordinate(t)
						if t.play(coor2)==True:
							t.back(1)
							return str(coor2)
	return PASS

def think(t, depth, ttt):
    if t.isGameOver():
        raise Exception("This match is over.")
    timeLimit = int(time.time()) + ttt
    cp = t.vez()
    if len(t.history) and t.history[-1]==PASS:
        b, w = t.count()
        originalNote = (b - w) if (cp == BLACK) else (w - b)
        if originalNote > 0:
            return mayPass(t)
    jogada=remember(t)
    if jogada and (jogada!=PASS):
        return jogada
    probabilities = dict()
    lenthistory=len(t.history)
    while time.time() < timeLimit:
        for _ in range(depth):
            if t.isGameOver():
                break
            coor = getEmptyCoordinate(t)
            if t.play(coor)==False:
                t.play(PASS)
        b, w = t.count()
        n = (b - w) if (cp == BLACK) else (w - b)
        score = 1 if n > 0 else -1
        coor = t.history[lenthistory]
        probabilities[coor]=probabilities.get(coor,0)+score
        how_many=len(t.history)-lenthistory
        t.back(how_many=how_many)
    m=max(probabilities,key=probabilities.get,default=PASS)
    if (m==PASS) or (probabilities[m]<0):
        return mayPass(t)
    return m

def paintFirstScreen(screen):
    halfHeight=MODE[1]//2
    quarteHeight=halfHeight//2
    screen.fill(BGCOLOR)
    font=pygame.font.SysFont (pygame.font.get_default_font(), FONT_SIZE)
    blackImage=font.render("Black",1,(0,0,0),BGCOLOR)
    whiteImage=font.render("White",1,(255,255,255),BGCOLOR)
    screen.blit(blackImage, (0,quarteHeight))
    screen.blit(whiteImage, (0, halfHeight+quarteHeight))
    pygame.display.flip()

def firstScreen(screen):
    halfWidth=MODE[0]//2
    halfHeight=MODE[1]//2
    while True:
        paintFirstScreen(screen)
        ev=pygame.event.wait()
        if ev.type==pygame.QUIT:
            exit()
        if ev.type==pygame.MOUSEBUTTONDOWN:
            x,y=ev.pos
            if (x>=halfWidth) or (y>=MODE[1]):
                continue
            if y<=halfHeight:
                return BLACK
            else:
                return WHITE

def paintSquare(screen,center,side):
    hs=side//2
    p0=(center[0]-hs,center[1]-hs)
    p1=(center[0]+hs,center[1]-hs)
    p2=(center[0]+hs,center[1]+hs)
    p3=(center[0]-hs,center[1]+hs)
    color=(0,0,255)
    bold=3
    pygame.draw.line(screen,color,p0,p1,bold)
    pygame.draw.line(screen,color,p1,p2,bold)
    pygame.draw.line(screen,color,p2,p3,bold)
    pygame.draw.line(screen,color,p3,p0,bold)

selected=None

def paintBoard(screen,t,hp):
    screen.fill(BGCOLOR)
    s=str(t)
    y=0
    x=0
    font=pygame.font.get_default_font()
    font=pygame.font.SysFont(font,FONT_SIZE)
    for c in s:
        if c=="\n":
            y+=FONT_SIZE
            x=0
            continue
        color=(127,127,127)
        if c==BLACK:
            color=(0,0,0)
        elif c==WHITE:
            color=(255,255,255)
        if (c==ESPACO) and (selected!=None) and ((selected.x==x//FONT_SIZE) and (selected.y==y//FONT_SIZE)):
            color=(255,0,0)
        image=font.render(c,1,color,BGCOLOR)
        screen.blit(image,(x,y))
        x+=FONT_SIZE
    if t.history and (t.history[-1]!=PASS):
        h=t.history[-1]
        coor=Coordenada(h)
        x=coor.x*FONT_SIZE
        y=coor.y*FONT_SIZE
        des=(FONT_SIZE//4)
        paintSquare(screen,(x+des,y+des),FONT_SIZE//5)
    if (hp==t.vez()) and not t.isGameOver():
        red=(255,0,0)
        confirmImage=font.render("Confirm",1,red,BGCOLOR)
        passImage=font.render("Pass",1,red,BGCOLOR)
        x=FONT_SIZE*SIDE
        screen.blit(confirmImage, (x,MODE[1]//4))
        screen.blit(passImage, (x,(MODE[1]//4)*3))
    pygame.display.flip()

def publishHistory(history):
    url=baseAddress+"links?side="+str(SIDE)
    response=requests.get(url=url,timeout=60)
    rsc=response.status_code
    if rsc!=200:
        e=RuntimeError("links status code="+str(rsc))
        raise e
    links_html=str(response.content)
    tid=""
    bid=""
    wid=""
    li=links_html.index("tid=")+4
    while links_html[li].isdigit():
        tid+=links_html[li]
        li+=1
    li=links_html.index("bid=")+4
    while links_html[li].isdigit():
        bid+=links_html[li]
        li+=1
    li=links_html.index("wid=")+4
    while links_html[li].isdigit():
        wid+=links_html[li]
        li+=1
    vez=BLACK
    for h in history:
        url=baseAddress+"table?tid="+tid
        if vez==BLACK:
            url+="&bid="+bid
        else:
            url+="&wid="+wid
        url+="&mov="+h
        response=requests.get(url,timeout=60)
        rsc=response.status_code
        if rsc!=200:
            e=RuntimeError("mov="+h+
            "\tstatus code="+str(rsc))
            raise e
        if vez==BLACK:
            vez=WHITE
        else:
            vez=BLACK
    html=str(response.content)
    if "<p>Game Over</p>" in html:
        return tid
    if "<p>This match was deleted.</p>" in html:
        return "deleted"
    return "error"

def paintScore(screen,t):
    msg=""
    black, white = t.count()
    if black>white:
        msg="B+"+str(black-white)
    else:
        msg="W+"+str(white-black)
    font=pygame.font.get_default_font()
    font=pygame.font.SysFont(font,FONT_SIZE)
    image=font.render(msg,1,(0,0,255),BGCOLOR)
    screen.blit(image, (FONT_SIZE, FONT_SIZE*SIDE))
    pygame.display.flip()

def paintPublished(screen,msg):
       font=pygame.font.get_default_font()
       font=pygame.font.SysFont(font,32)
       image=font.render(msg,1,(0,255,0),BGCOLOR)
       screen.blit(image,((MODE[0]//2)+FONT_SIZE,MODE[1]//2))
       pygame.display.flip()

def main():
    global selected
    pygame.init()
    screen=pygame.display.set_mode(MODE)
    baixar_banco()
    load()
    hp=firstScreen(screen)
    ttt = 30
    depth = SIDE * SIDE
    t = Tabuleiro()
    while not t.isGameOver():
        paintBoard(screen,t,hp)
        paintScore(screen,t)
        if hp == t.vez():
            ev=pygame.event.wait()
            if ev.type==pygame.QUIT:
                exit()
            paintBoard(screen,t,hp)
            paintScore(screen,t)
            if ev.type==pygame.MOUSEBUTTONDOWN:
                x,y=ev.pos
                if y>MODE[1]:
                    continue
                if x<(SIDE*FONT_SIZE):
                    x=x//FONT_SIZE
                    y=y//FONT_SIZE
                    selected=Coordenada(x,y)
                else:
                    if y<MODE[1]//2:
                        if selected:
                            t.play(selected)
                            selected=None
                    else:
                        t.play(PASS)
                        selected=None
        else:
            coor = think(t, depth, ttt)
            t.play(coor)
    paintBoard(screen,t,hp)
    paintScore(screen,t)
    pubed=""
    if publish:
        try:
           pubed=publishHistory(t.history)
        except Exception as e:
            pubed=str(e)
        finally:
           paintPublished(screen,pubed)
    while True:
        ev=pygame.event.wait()
        if ev.type==pygame.QUIT:
            exit()
        else:
            paintBoard(screen,t,hp)
            paintScore(screen,t)
            paintPublished(screen,pubed)
    
if __name__=="__main__":
    main()
