diff --git a/default.css b/default.css index b2a3cfc..30111d7 100644 --- a/default.css +++ b/default.css @@ -45,7 +45,6 @@ body { height: 120px; margin: 0; padding: 0; - background-image: url(cryptobox-img/vault_pingu.png); background-position: top right; background-attachment: scroll; background-repeat: no-repeat; @@ -247,13 +246,13 @@ input#submit:hover { cursor: pointer; } -/* the submit buttons have to have id="goban" for the following style, for use in goban display ------------- */ -/*input#goban { + +input { padding: 0px; border: 0px; margin: 0px; } -*/ + /*input#goban:hover { padding: 0px; border: 0px; diff --git a/deletegame.py b/deletegame.py index c991e51..e1402c5 100644 --- a/deletegame.py +++ b/deletegame.py @@ -1,4 +1,6 @@ -import psql,login +import psql,login,helper + +#helper = apache.import_module("helper") def get_game_slot_of_game(player, gamename): """ @@ -18,7 +20,7 @@ def main(req,form): manage the removal of game from game slots of players and delete game from database. """ - helper.debug(req,form,str(form.keys())+" sessionid in form:"+form["sessionid"]+"
") + helper.debug(req,form,"
"+str(form.keys())+" sessionid in form:"+form["sessionid"]+"
") try: gamename = form["game"] except: diff --git a/documentation/development/INSTALL b/documentation/development/INSTALL index 12543d2..5308a4e 100644 --- a/documentation/development/INSTALL +++ b/documentation/development/INSTALL @@ -1,3 +1,5 @@ +-------------------- installing GnuGo ---------------------- +apt-get install gnugo ----------------- creating the database -------------------- #installing PostgreSQL the Debian way apt-get install postgresql diff --git a/documentation/development/README_DEVEL b/documentation/development/README_DEVEL index 965b644..6b80047 100644 --- a/documentation/development/README_DEVEL +++ b/documentation/development/README_DEVEL @@ -12,22 +12,30 @@ imgsource/ with the desired ones. then run generate_goban_images.py and the new images get written to img/. ------------------------------------------ overview of the files: -main.py - the main script. Essentially a wrapper that prints the +main.py -the main script. Essentially a wrapper that prints the traceback if something went wrong. -login.py - this file contains functions for login and game +login.py -this file contains functions for login and game selection/modification. -goban.py - code for displaying a goban as a html page. -psql.py - all functions directly accessing the database. -init_webgo.py - re-initialize the whole game. deletes everything and +goban.py -code for displaying a goban as a html page. +psql.py -all functions directly accessing the database. +init_webgo.py -re-initialize the whole game. deletes everything and sets some defaults. -helper.py - small functions for string manipulation etc. +helper.py -small functions for string manipulation etc. generate_goban_images.py - - recreate the goban images. just run from console. + -recreate the goban images. just run from console. +gnugo.py -contains functions using gnugo in gtp mode. used for + evaluating wether or not a move is legal. +filehandling.py -contains functions for, well, file handling ;> ------------------------------------------ coding guidelines tab == 4 whitespaces. -function names are lowercase with _ as a seperator. +function names are all lowercase with _ as a seperator. +------------------------------------------ +how is the data saved internally: +for each go game a table is generated. this table has a 'sgf' field containing the current board in sgf file format (needed for gnugo move validation). It also has fields for each goban field. The 'sgf' entry is the SPOT for the current state of the board. +If the board gets changed, this means the 'sgf' file gets changed and then the changed fields in the table get +updated accordingly. This is a bit unclean, but it means I do not have to write a sgf parser ;> ------------------------------------------ known bugs: "DatabaseError: error 'ERROR: current transaction is aborted, commands ignored until end of transaction block" diff --git a/documentation/development/ROADMAP b/documentation/development/ROADMAP index ae4848b..88004e4 100644 --- a/documentation/development/ROADMAP +++ b/documentation/development/ROADMAP @@ -38,12 +38,12 @@ m6: manage users and game sessions inside database username and password and is then given the choice of game to play. If there is only one active game, directly present the goban. - . <--- YOU ARE HERE. - m7: implement Go rules Do not accept player moves which violate go rules. Remove beaten stones. Count and display the score for each player. + . <--- YOU ARE HERE. + m8: make it pretty, make it fast. Create nicer images replacing the old ones which were just for testing Find bottlenecks and optimize them for speed. diff --git a/documentation/development/TODO b/documentation/development/TODO index 1e74532..d4944a3 100644 --- a/documentation/development/TODO +++ b/documentation/development/TODO @@ -1 +1,6 @@ -- hoshi Bilder fuer weisse und schwarze Steine erstellen \ No newline at end of file +- hoshi Bilder fuer weisse und schwarze Steine erstellen +- remove temporarily created files ;> +- what happens if a game is finished? +- add a description field for a goban table +- add a date-of-creation-creation field and a last-changed-field to goban table +- display game list more nicely with players and last change. \ No newline at end of file diff --git a/filehandling.py b/filehandling.py new file mode 100644 index 0000000..a2a1350 --- /dev/null +++ b/filehandling.py @@ -0,0 +1,85 @@ +import os,tempfile,dircache,string,commands + + +def gen_temp_dir(prefix=None): + """generate a secure temporary directory. returns name of dir.""" + dirname=tempfile.mkdtemp("",prefix) + dirname+="/" + return dirname + + +def read_file(filename): + """ + read from the file whose name is given + @param filename String : name of file to read from + @return String: content of given file or None + """ + try: + f = open(filename,"r") + filecontent = f.read() + f.close() + except: + #TODO:if debug >=1: + print "(EE)[%s]: \"%s\" is not readable!"%(__name__, filename) + return "" + return filecontent + + + + +def is_dir_readable(path): + """ + Gets the name of a directory. + returns True if dir is readable, else False. + """ + return os.access(path,os.R_OK) + + + +def write_file(filename,content): + """ + Write content to the given filename. + gets: filename,content. + """ + try: + f = open(filename,"w")#oeffnen und schliessen => + f.close() #datei ist jetzt genullt + f = open(filename,"a") #anhaengend oeffnen + f.write(content) + f.close() + return "" + except: + #TODO: debug + #if self.debug >=1: + print "(EE)[%s]: \"%s\" is not writeable!"%(__name__, filename) + return filename + +def basename(filename): + return os.path.basename(filename) + + +def make_all_dirs_absolute(prefix,dirlist): + """ + this function gets an absolute pathname and a list of pathnames + relative to the first. It returns this list, but with absolute + entries. + """ + ret=[] + for element in dirlist: + ret.append(os.path.normpath(prefix+"/"+element)) + return ret + + +def gen_temp_dir(prefix='pk'): + '''returns the name of a secure temporary directory + with optional prefix as parameter''' + dirname=tempfile.mkdtemp("",prefix) + dirname+="/" + return dirname + +def gen_temp_file(suffix="--gnugo"): + """ + returns the name of a generated temporay file. + optionally gets a suffix for the filename. + """ + return tempfile.mkstemp(suffix)[1] \ No newline at end of file diff --git a/gnugo.py b/gnugo.py new file mode 100644 index 0000000..ae81730 --- /dev/null +++ b/gnugo.py @@ -0,0 +1,247 @@ +import string,sys,popen2 +import helper,filehandling + +gnugocommand = "/usr/games/gnugo --mode gtp" + + +showboard = """ += + A B C D E F G + 7 . . . . . . . 7 + 6 . . . . . . . 6 + 5 . . + . + . . 5 + 4 . . X X . . . 4 + 3 . . O . + . . 3 + 2 . . . . . . . 2 WHITE (O) has captured 0 stones + 1 . . . . . . . 1 BLACK (X) has captured 0 stones + A B C D E F G + +""" + + + +def is_int( str ): + """ Is the given string an integer? """ + ok = 1 + try: + num = int(str) + except ValueError: + ok = 0 + return ok + + +def showboard_to_goban_dict(showboard): + """ + gets a string containing the result of the 'showboard' command from + gnugo --mode gtp. + returns a goban dictionary . + """ + gobandict= {} + + showboardlist = string.split(showboard,'\n') + while '' in showboardlist: + showboardlist.remove('') + #remove '=' + if string.find(showboardlist[0],"=")>=0: + showboardlist.remove(showboardlist[0]) + #get boardsize + tmplist = string.split(showboardlist[0]," ") + while '' in tmplist: + tmplist.remove('') + boardsize = len(tmplist) + #remove first A B C D ... + showboardlist.remove(showboardlist[0]) + #remove last A B C D ... + showboardlist.remove(showboardlist[-1]) + + for i in range(0,boardsize): + #make string to list + linelist = string.split(showboardlist[i],' ') + #clean list from '' entries + while '' in linelist: + linelist.remove('') + # remove numbers + for p in range(0,boardsize+1): + if is_int(linelist[p]): + linelist.remove(linelist[p]) + #print linelist,'\n' + + #now fill dictionary: + for j in range(0,boardsize): + if (linelist[j] == ".") or (linelist[j] == "+"): + gobandict[(i+1,j+1)] = 0 #empty + if linelist[j] == "O": + gobandict[(i+1,j+1)] = 1 #white stone + if linelist[j] == "X": + gobandict[(i+1,j+1)] = 2 #black stone + + #check for info of caputered stones: + if len(linelist) > boardsize: + if 'WHITE' in linelist: + whitecapture = "white has captured %s stones." % linelist[-2] + #print whitecapture + elif 'BLACK' in linelist: + blackcapture = "black has captured %s stones." % linelist[-2] + gobandict["size"] = boardsize + + return gobandict + +def sgf_to_goban_dict(gobandict, filename=""): + """ + gets a goban dictionary and optionally a temporary filename. + saves string to file, loads in gnugo, does showboard, convert showboard to gobandict. + return gobandict 'lite' (just coordinates and size) + """ + sgf = gobandict["sgf"] + #generate tmpfile + if filename == "": + filename = filehandling.gen_temp_file() + #write sgf data to file + filehandling.write_file(filename,sgf) + #open connection to gnugo: + conn = GTP_connection(gnugocommand) + #load sgf file in gnugo + result = conn.exec_cmd("loadsgf "+filename) + #make move (looks like 'play black F2' + result = conn.exec_cmd("showboard") + #convert to goban dict + ret = showboard_to_goban_dict(result) + result = conn.exec_cmd("quit") + return ret + + +### GTP_connection is based on twogtp.py packaged with gnugo. +class GTP_connection: + """ + gets a program call string, opens an interactive session with + program called with 'program'. GTP_connection.exec_cmd(command) + returns result of command. + # + # Class members: + # outfile File to write to + # infile File to read from + """ + def __init__(self, command): + try: + infile, outfile = popen2.popen2(command) + except: + print "popen2 failed" + self.infile = infile + self.outfile = outfile + + def exec_cmd(self, cmd): + self.outfile.write(cmd + "\n\n") + self.outfile.flush() + result = "" + line = self.infile.readline() + while line != "\n": + result = result + line + line = self.infile.readline() + # Remove trailing newline from the result + if result[-1] == "\n": + result = result[:-1] + if len(result) == 0: + return "ERROR: len = 0" + if (result[0] == "?"): + return "ERROR: GTP Command failed: " + result[2:] + if (result[0] == "="): + return result[2:] + return "ERROR: Unrecognized answer: " + result + + +def is_legal(gobandict,coords,req,form): + """ + gets a goban dict and a (x,y) tuple. + tests wether proposed move is legal. + returns True or False. + """ + size = gobandict["size"] + turn = gobandict["turn_number"] + #who's turn is it? + color = ["white","black"][turn % 2] #even turn: white plays, else black plays + #convert given coordinates + gnucoords = " " + helper.dict_coords_to_gnugo_coords(coords,size) + #open connection to gnugo: + conn = GTP_connection(gnugocommand) + result = conn.exec_cmd("is_legal "+ color +gnucoords) + if result == "1": + return True + else: + return False + result = conn.exec_cmd("quit") + + +def create_sgf_file(size, filename=""): + """ + gets: board size, optionally a temporary filename to use. + returns the content of an empty sgf file. + """ + conn = GTP_connection(gnugocommand) + result = conn.exec_cmd("boardsize "+str(size)) + if filename == "": + filename = filehandling.gen_temp_file() + result = conn.exec_cmd("printsgf "+filename) + ret = filehandling.read_file(filename) + result = conn.exec_cmd("quit") + return ret + + +def make_move_in_sgf(req,form,gobandict,coords,filename = ""): + """ + gets: goban dict, (x,y) tuple for move, optionally a filename for tempfile. + writes the string to a file, opens gnugo, makes the move, writes new sgf + to temporary file, reads content of temporary file. + returns: sgf string of new file. + """ + size = gobandict["size"] + turn = gobandict["turn_number"] + sgf = gobandict["sgf"] + + #convert given coordinates + gnucoords = " " + helper.dict_coords_to_gnugo_coords(coords,size) + # get current player + #even turn: white plays, else its blacks turn + color = ["white","black"][turn % 2] + helper.debug(req,form,"color: %s -- turn: %s " % (color,turn)) + #generate tmpfile + if filename == "": + filename = filehandling.gen_temp_file() + #write sgf data to file + filehandling.write_file(filename,sgf) + #open connection to gnugo: + conn = GTP_connection(gnugocommand) + #load sgf file in gnugo + result = conn.exec_cmd("loadsgf "+filename) + #make move (looks like 'play black F2' + result = conn.exec_cmd("play "+color+gnucoords) + #save result to file + result = conn.exec_cmd("printsgf "+filename) + #read out new file + sgf = filehandling.read_file(filename) + result = conn.exec_cmd("quit") + return sgf + + +if __name__ == '__main__': + #showboard_to_goban_dict(showboard) + import popen2 + + conn = GTP_connection("gnugo --mode gtp") + result = conn.exec_cmd("boardsize 7") + result = conn.exec_cmd("genmove black") + result = conn.exec_cmd("genmove white") + result = conn.exec_cmd("printsgf test.sgf") + result = conn.exec_cmd("quit") + conn = GTP_connection("gnugo --mode gtp") + result = conn.exec_cmd("loadsgf test.sgf") + result = conn.exec_cmd("showboard") + print result,"\n----\n" + print showboard_to_goban_dict(result) + print create_sgf_file(7) + + + #loadsgf + #printsgf + #is_legal black F2 + #dict_coords_to_gnugo_coords(coords) translates (6,6) to "F2" + diff --git a/goban.py b/goban.py index 65ccb8b..dcb9506 100755 --- a/goban.py +++ b/goban.py @@ -4,8 +4,7 @@ DEBUG = 1 import sys,string import cgi -import pickle -import psql,helper +import psql,helper,gnugo picklefile = "goban.pickledump" @@ -25,7 +24,7 @@ def display_goban(goban,req,form): hoshis9x9 = [(3,3),(3,7),(5,5),(7,3),(7,7)] - data += '

' + data += '\n

\n\n' data += """ @@ -50,30 +49,30 @@ def display_goban(goban,req,form): sy = str(y) # check position: if (x == 1) and (y == 1): # upper left - data += '' + data += '' elif (x == 1) and (y == size): # upper right - data += '
' + data += '' elif (x == size) and (y == size): # lower right - data += '
' + data += '
\n' elif (x == size) and (y == 1): # lower left - data += '' + data += '' elif (y == 1): #left line - data += '' + data += '' elif (x == 1): # top line - data += '' + data += '' elif (y == size): # right line - data += '
' + data += '' elif (x == size): #bottom line - data += '' + data += '' else: # hoshi or empty inner field - defaultfield = '' + defaultfield = '' #too lazy to make special images for hoshi fields with stones: if goban[(x,y)] == 1: - hoshifield = '' + hoshifield = '' elif goban[(x,y)] == 2: - hoshifield = '' + hoshifield = '' else: #empty hoshi - hoshifield = '' + hoshifield = '' if size == 19: # 9 hoshis if (x,y) in hoshis19x19: data += hoshifield @@ -100,7 +99,7 @@ def display_goban(goban,req,form): -def process_form(req,form,goban): +def process_form(req,form,gobandict): """ gets a goban dictionary. @@ -108,34 +107,65 @@ def process_form(req,form,goban): if the goban has been clicked, return a (x,y) tuple of the position. """ + ret = "" #if form == empty (which means if page is displayed for the first time): if form.keys() != []: #cut out the name of the clicked button for item in form.keys(): if string.find(item,").x")>0: - namestring = string.split(item,".x")[0] - - position = helper.string_to_tuple(namestring) - ret = set_stone(goban, position) - if (type(ret) == type("")): - return (goban,ret) #return old goban and error string - else: - goban = ret - return (goban,"") # return new goban and empty string + coordstring = string.split(item,".x")[0] + position = helper.string_to_tuple(coordstring) + ret = set_stone(gobandict, position,req,form) + return ret -def set_stone(goban, position): +def set_stone(gobandict, position,req,form): """gets a goban dictionary and a (x,y)-tuple. Returns a modified goban.""" - turn = goban["turn_number"] - if (goban[position] == 0): #empty field - goban[position] = (turn % 2) + 1 #even turn: white stone (1), else black stone(2) - goban["turn_number"] += 1 - #now write changed values to database - psql.update_goban_field(goban["name"],position[0],position[1],(turn % 2) + 1) - psql.update_turn_number(goban["name"],goban["turn_number"]) - return goban - else: + size = gobandict["size"] + name = gobandict["name"] + turn = gobandict["turn_number"] + if (gobandict[position] == 0): #empty field + if gnugo.is_legal(gobandict,position,req,form): #gnugo says the move is ok + #let gnugo make the above move, let gnugo write move to file + new_sgf = gnugo.make_move_in_sgf(req,form,gobandict,position) + #write new sgf file into database + psql.update_goban_table_field(name,"x1","sgf",new_sgf) + #...and in current gobandict + gobandict["sgf"] = new_sgf + #now convert sgf to gobandict coordinates. + gobanlite = gnugo.sgf_to_goban_dict(gobandict) + #merge new coordinates with gobandict and update database where necessary + (new_gobandict, something_changed) = update_goban_dict_and_table(gobandict,gobanlite) + #and finally: + if something_changed: + psql.update_turn_number(name,turn+1) + helper.debug(req,form,"updated turn b/c gobandict has changed") + else: + helper.debug(req,form,"gobandict has not been changed, leaving turn_number untouched") + helper.debug(req,form,"set_stone:game name: %s, turn: %s, value at position: %s" % (name,turn,new_gobandict[position])) + return "" + else: #move not ok + return "This is not a legal move (says Gnugo)." + else: #position not empty return "Could not make move: Field not empty." + +def update_goban_dict_and_table(gobandict,gobanlite): + """ + gets a gobandict and a gobdict light (just coordinates and size). + updates the fields in gobandict and database. + returns changed gobandict and True (or False) if something has been changed (or not). + """ + tf = False + for key in gobanlite.keys(): + if gobandict[key] != gobanlite[key]: + #found difference in dicts. + #update gobandict + gobandict[key] = gobanlite[key] + #update databse table. the only valid difference can be changed goban field positions. + psql.update_goban_field(gobandict["name"],key[0],key[1],gobandict[key]) + tf = True + return gobandict,tf + ############################################################################### diff --git a/helper.py b/helper.py index 6c523a7..62cf77e 100644 --- a/helper.py +++ b/helper.py @@ -5,10 +5,7 @@ DEBUG = 1 def header(): """return html header""" data = """ - - - - + WebGo @@ -17,8 +14,8 @@ def header(): -

WebGo

+ """ return data @@ -30,9 +27,9 @@ def debug(req,form, optstr = ""): """ if DEBUG: if optstr == "": - req.write(str(form.keys())) + req.write("Debug: "+str(form.keys())+"
\n") else: - req.write(optstr) + req.write("Debug: "+optstr+"
\n") def footer(): """return html footer""" @@ -87,4 +84,26 @@ def string_to_tuple(str): returnlist.append(int(item)) except: #empty string returnlist.append(-1) - return tuple(returnlist) \ No newline at end of file + return tuple(returnlist) + +def dict_coords_to_gnugo_coords(coords,size): + """ + gets a (x,y) coordinate tuple and boardsize. + returns a string in gnugo syntax. examples: + gets (1,1), returns "A7". + gets (6,2), returns "B2". + """ + letterlist = [" "] + letterlist.extend(list(string.letters[26:])) + letterlist.remove("I") + letter = letterlist[coords[1]] + digit = size+1-coords[0] + return letter+str(digit) + +def test(): + print dict_coords_to_gnugo_coords((6,5),7) + +if __name__ == "__main__": + test() + + \ No newline at end of file diff --git a/playgame.py b/playgame.py index 7655ea1..4599630 100644 --- a/playgame.py +++ b/playgame.py @@ -9,15 +9,13 @@ def is_my_turn(req,form,gobandict): check wether or not the current this is the players turn. return true or false. """ - #INFO: player1 is black,player2 is white. black starts the game. me = form["username"] player1 = gobandict["player1"] player2 = gobandict["player2"] - #get turn_number. turn_number = gobandict["turn_number"] - #if turn_number modulo 2 == 0: are we player two? + #if player 2 can play: are we player two? # yes:return True, else return False if turn_number % 2 == 0: if me == player2: @@ -41,42 +39,46 @@ def main(req,form): """ display selected goban and manage user input. """ - helper.debug(req,form,str(form.keys())+"
") - + try: gamename = form["game"] except: gamename = "" if gamename != "": - + data = "" + req.write(helper.header()) + #helper.debug(req,form,str(form.keys())) #read goban table from database tmplist = psql.read_table(gamename) #make a dictionary out of the list gobandict = psql.fetchall_list_to_goban_dict(tmplist) #check if user has already clicked onto a field: - foundx = False + click_on_field = False for item in form.keys(): if string.find(item,").x") > 0: - foundx = True - if foundx: + click_on_field = True + if click_on_field: + #if yes: is this the user's turn? then process form. if is_my_turn(req,form,gobandict): - (gobandict,retstring) = goban.process_form(req,form,gobandict) - - #do stuff - data = helper.header() - - - data += "Turn number: "+str(gobandict["turn_number"])+". " + helper.debug(req,form,"its my turn , i am going to process the form: --") + retstring = goban.process_form(req,form,gobandict) + if retstring != "": + helper.debug(req,form,"playgame.main says: "+str(retstring)) + else: + #reload gobandict, it has been changed. + gobandict = psql.fetchall_list_to_goban_dict(psql.read_table(gamename)) + else: + helper.debug(req,form,"its not my turn.") + data += """
Turn number: %s. %s plays.\n + """ % (str(gobandict["turn_number"]), ["White","Black"][int(gobandict["turn_number"] % 2)]) #check whether its our turn #if yes: print 'your move' and display goban and process move #if not: print '...s move' and display goban. if is_my_turn(req,form,gobandict): - data += ("Its your turn.
") + data += "
Its your turn.
\n" else: - data+= ("This is not your turn. You have to wait for the move of the other player.
") - - + data += "
This is not your turn. You have to wait for the move of the other player.
\n" #print goban data += goban.display_goban(gobandict,req,form) data += login.navigation_bar(req,form) diff --git a/psql.py b/psql.py index 33664e9..00dfc3b 100755 --- a/psql.py +++ b/psql.py @@ -1,4 +1,4 @@ -import helper +import helper,gnugo import pgdb,sys DEBUG = 1 @@ -50,6 +50,7 @@ def create_goban_table(player1,player2,size): name player1 player2 + sgf and the meaning of these fields: (xn,yn) is a field of the goban, @@ -58,8 +59,7 @@ def create_goban_table(player1,player2,size): (name,x1) is the name of this goban. (player1,x1) is the name of one player. (player2,x1) is the name of the other player. - - + (sgf,x1) contains a sgf file with the current board. """ tablename = helper.generate_game_name() data="line text" @@ -101,6 +101,12 @@ def create_goban_table(player1,player2,size): tmplist.append("player2") tmplist.append(player2) insert_into_table(tablename,str(tuple(tmplist))) + #empty sgf file as string + tmplist=[] + tmplist.append("sgf") + sgf = gnugo.create_sgf_file(size) + tmplist.append(sgf) + insert_into_table(tablename,str(tuple(tmplist))) return tablename @@ -324,6 +330,9 @@ def set_game_slot(username,gameslot,gamename): executestring ="UPDATE users SET %s = '%s' WHERE username = '%s'" %(gameslot,gamename, username) sql_one_liner(executestring) + + + ################# access of goban tables #################################################################### def get_players_for_game(tablename): @@ -361,6 +370,19 @@ def update_turn_number(table,new_number): """ update_goban_table_field(table,"x1","turn_number",new_number) +def get_sgf(table): + """ + gets a table name, + returns content of "sgf" field. + """ + cursor=db.cursor() + data="select x1 from %s where line='sgf';" % (table) + cursor.execute(data) + # Commit the changes + db.commit() + sgf = cursor.fetchone()[0] + cursor.close() + return sgf def fetchall_list_to_goban_dict(list):