Commit 707e794d authored by Sam Moore's avatar Sam Moore
Browse files

Lots of stuff happened

1. Argument parsing. We have it

2. GUI. We have it

3. Networking. We have it.

4. Timeouts. We have those. Sort of. Not for Windows.

5. Help page. We have that too.
	I think you'll find it quite... helpful

We still don't have log files. Most of previous TODOs still apply.
parent bfa63f1a
......@@ -35,7 +35,7 @@ class Agent(AgentRandom): # Inherits from AgentRandom (in qchess.py)
return float(self.value[piece.types[0]] + self.value[piece.types[1]]) / 2.0
# Score possible moves for the piece
# Highest score is 1.0 (which means: make this move!)
def prioritise_moves(self, piece):
#sys.stderr.write(sys.argv[0] + ": prioritise called for " + str(piece) + "\n")
......@@ -140,7 +140,7 @@ class Agent(AgentRandom): # Inherits from AgentRandom (in qchess.py)
if self.recurse_for >= 0:
opts = opts[0:self.recurse_for]
sys.stderr.write(sys.argv[0] + " : Before recurse, options are " + str(opts) + "\n")
#sys.stderr.write(sys.argv[0] + " : Before recurse, options are " + str(opts) + "\n")
# Take the best few moves, and recurse
for choice in opts[0:self.recurse_for]:
......@@ -162,7 +162,7 @@ class Agent(AgentRandom): # Inherits from AgentRandom (in qchess.py)
opts.sort(key = lambda e : e[1][1], reverse = True)
sys.stderr.write(sys.argv[0] + " : After recurse, options are " + str(opts) + "\n")
#sys.stderr.write(sys.argv[0] + " : After recurse, options are " + str(opts) + "\n")
self.depth -= 1
return list(opts[0])
......
# Makefile that builds qchess.py from the component files
TARGET = qchess.py
COMPONENTS = piece.py board.py player.py network.py thread_util.py game.py graphics.py main.py
#COMPONENTS=$(shell ls *.py | tr '\t' '\n' | grep -v $(TARGET))
$(TARGET) : $(COMPONENTS)
echo "#!/usr/bin/python -u" > $(TARGET)
for f in $(COMPONENTS); do echo "# +++ $$f +++ #" >> $(TARGET); cat $$f >> $(TARGET); echo "# --- $$f --- #" >> $(TARGET); done
echo "# EOF - created from make on $$(date)" >> $(TARGET)
chmod u+x $(TARGET)
clean :
rm -f *~
rm -f *.pyc
rm -f $(TARGET)
......@@ -96,7 +96,7 @@ class Board():
if window == None:
return
for p in self.pieces["white"] + self.pieces["black"]:
p.draw(window, grid_sz)
p.draw(window, grid_sz, self.style)
# Draw the board in a pygame window
def display(self, window = None):
......
NAME
qchess.py - Play quantum chess
SYNOPSIS
qchess.py [OPTIONS] [white] [black]
DESCRIPTION
An implementation of Quantum Chess as originally described and implemented here:
http://research.cs.queensu.ca/Parallel/QuantumChess/QuantumChess.html
Reimplemented for UCC::Progcomp 2013
http://progcomp.ucc.asn.au
IMPORTANT:
- This version does not implement castling or en passen rules.
- If a piece currently in a pawn state moves into the opposing back row, that state always becomes a queen.
- (The other state of the piece is unaffected).
ARGUMENTS
If no arguments are given, a window should appear asking you to pick each player.
Then the game will commence using default values.
white, black
Each of the two players in order. They need not be provided if graphics is enabled (default).
Any arguments that do not begin with a hyphen (-) are treated as the player arguments in the order they appear.
Player arguments that begin with '@' are treated as special players:
@human
A human player; if graphics are enabled, this players turns are made through the GUI
@network[:address]
A player over a network connection.
For example, if [email protected] wants to play [email protected]:
[email protected]:~$ ./qchess.py @network @human
[email protected]:~$ ./qchess.py @human @network:host1
IMPORTANT: Only ONE of the games should give the other's address.
OPTIONS
--help
Print this page
--graphics
Disable/Enable the GUI
If graphics are enabled (default), then the user will be prompted to choose any of the two players not supplied as arguments.
--file[=filename]
Replay a game saved in file, or read from stdin if no filename given
--log[=filename]
Log moves to a file or stdout if no filename given
--delay[=time]
The game pauses between moves so that it can be followed by a human observer.
This option can be used to change the delay. If no time is given, the delay is disabled.
If graphics are enabled (default), the delay is 0.5s by default.
If graphics are disabled, there is no delay unless this option is used.
--timeout[=time]
Set the maximum time in seconds to wait before declaring an AI program unresponsive.
If no time is given, the timeout is disabled.
By default the timeout is disabled.
--classical
If this option is used, the game will treat pieces "classically", ie: as in standard chess.
Note that the game does not enforce rules related to check and checkmate.
--quantum
The game uses the quantum chess representation of pieces (default).
AUTHOR
Written for the UCC Programming Competition 2013 by Sam Moore.
UCC::Progcomp home page: http://progcomp.ucc.asn.au
REPORTING BUGS
Report bugs to [email protected]
Join IRC channel #progcomp on irc://irc.ucc.asn.au
COPYRIGHT
Copyright 2013 The University Computer Club, Inc.
Contact [email protected]
......@@ -22,8 +22,7 @@ class GameThread(StoppableThread):
self.state["turn"] = p.base_player # "turn" contains the player who's turn it is
else:
self.state["turn"] = p
#try:
if True:
try:
[x,y] = p.select() # Player selects a square
if self.stopped():
break
......@@ -81,18 +80,18 @@ class GameThread(StoppableThread):
graphics.state["moves"] = None
# Commented out exception stuff for now, because it makes it impossible to tell if I made an IndentationError somewhere
#except Exception,e:
#result = "ILLEGAL " + e.message
except Exception,e:
result = e.message
#sys.stderr.write(result + "\n")
#self.stop()
#with self.lock:
# self.final_result = self.state["turn"].colour + " " + "ILLEGAL"
self.stop()
with self.lock:
self.final_result = self.state["turn"].colour + " " + e.message
if self.board.king["black"] == None:
if self.board.king["white"] == None:
with self.lock:
self.final_result = "DRAW"
self.final_result = self.state["turn"].colour + " DRAW"
else:
with self.lock:
self.final_result = "white"
......
......@@ -71,7 +71,10 @@ class GraphicsThread(StoppableThread):
if event.type == pygame.QUIT:
if isinstance(game, GameThread):
with game.lock:
game.final_result = "terminated"
game.final_result = ""
if game.state["turn"] != None:
game.final_result = game.state["turn"].colour + " "
game.final_result += "terminated"
game.stop()
self.stop()
break
......@@ -272,15 +275,157 @@ class GraphicsThread(StoppableThread):
pygame.display.flip()
def getstr(self, prompt = None):
s = pygame.Surface((self.window.get_width(), self.window.get_height()))
s.blit(self.window, (0,0))
result = ""
while True:
#print "LOOP"
if prompt != None:
self.message(prompt)
self.message(result, pos = (0, 1))
pygame.event.pump()
for event in pygame.event.get():
if event.type == pygame.QUIT:
return None
if event.type == pygame.KEYDOWN:
if chr(event.key) == '\r':
return result
result += str(chr(event.key))
if event.key == pygame.K_BACKSPACE:
result = result[0:len(result)-1]
self.window.blit(s, (0,0)) # Revert the display
continue
try:
if event.unicode == '\r':
return result
result += str(event.unicode)
except:
continue
# Function to pick a button
def SelectButton(self, choices, prompt = None, font_size=32):
self.board.display_grid(self.window, self.grid_sz)
if prompt != None:
self.message(prompt)
font = pygame.font.Font(None, font_size)
targets = []
sz = self.window.get_size()
for i in range(len(choices)):
c = choices[i]
text = font.render(c, 1, pygame.Color(0,0,0))
p = (sz[0] / 2 - (1.5*text.get_width())/2, sz[1] / 2 +(i-1)*text.get_height()+(i*2))
targets.append((p[0], p[1], p[0] + 1.5*text.get_width(), p[1] + text.get_height()))
while True:
mp =pygame.mouse.get_pos()
for i in range(len(choices)):
c = choices[i]
if mp[0] > targets[i][0] and mp[0] < targets[i][2] and mp[1] > targets[i][1] and mp[1] < targets[i][3]:
font_colour = pygame.Color(255,0,0)
box_colour = pygame.Color(0,0,255,128)
else:
font_colour = pygame.Color(0,0,0)
box_colour = pygame.Color(128,128,128)
text = font.render(c, 1, font_colour)
s = pygame.Surface((text.get_width()*1.5, text.get_height()), pygame.SRCALPHA)
s.fill(box_colour)
pygame.draw.rect(s, (0,0,0), (0,0,1.5*text.get_width(), text.get_height()), self.grid_sz[0]/10)
s.blit(text, ((text.get_width()*1.5)/2 - text.get_width()/2 ,0))
self.window.blit(s, targets[i][0:2])
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.QUIT:
return None
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
for i in range(len(targets)):
t = targets[i]
if event.pos[0] > t[0] and event.pos[0] < t[2]:
if event.pos[1] > t[1] and event.pos[1] < t[3]:
return i
#print "Reject " + str(i) + str(event.pos) + " vs " + str(t)
# Function to pick players in a nice GUI way
def SelectPlayers(self, players = []):
missing = ["white", "black"]
for p in players:
missing.remove(p.colour)
for colour in missing:
choice = self.SelectButton(["human", "agent", "network"],prompt = "Choose " + str(colour) + " player", font_size=32)
if choice == 0:
players.append(HumanPlayer("human", colour))
elif choice == 1:
try:
import Tkinter
from tkFileDialog import askopenfilename
root = Tkinter.Tk() # Need a root to make Tkinter behave
root.withdraw() # Some sort of magic incantation
path = askopenfilename(parent=root, initialdir="../agents",title=
'Choose an agent.')
if path == "":
return self.SelectPlayers()
players.append(make_player(path, colour))
except Exception,e:
print "Exception was " + str(e.message)
p = None
while p == None:
self.board.display_grid(self.window, self.grid_sz)
pygame.display.flip()
path = self.getstr(prompt = "Enter path:")
if path == None:
return None
if path == "":
return self.SelectPlayers()
try:
p = make_player(path, colour)
except:
self.board.display_grid(self.window, self.grid_sz)
pygame.display.flip()
self.message("Invalid path!")
time.sleep(1)
p = None
players.append(p)
elif choice == 2:
address = ""
while address == "":
self.board.display_grid(self.window, self.grid_sz)
address = self.getstr(prompt = "Address? (leave blank for server)")
if address == None:
return None
if address == "":
address = None
continue
try:
map(int, address.split("."))
except:
self.board.display_grid(self.window, self.grid_sz)
self.message("Invalid IPv4 address!")
address = ""
players.append(NetworkReceiver(colour, address))
else:
return None
#print str(self) + ".SelectPlayers returns " + str(players)
return players
......@@ -18,49 +18,160 @@ import time
turn_delay = 0.5
[game, graphics] = [None, None]
def make_player(name, colour):
if name[0] == '@':
if name[1:] == "human":
return HumanPlayer(name, colour)
s = name[1:].split(":")
if s[0] == "network":
address = None
if len(s) > 1:
address = s[1]
return NetworkReceiver(colour, address)
else:
return AgentPlayer(name, colour)
# The main function! It does the main stuff!
def main(argv):
# Apparently python will silently treat things as local unless you do this
# But (here's the fun part), only if you actually modify the variable.
# For example, all those 'if graphics_enabled' conditions work in functions that never say it is global
# Anyone who says "You should never use a global variable" can die in a fire
global game
global graphics
# Magical argument parsing goes here
# if len(argv) == 1:
# players = [HumanPlayer("saruman", "white"), AgentRandom("sabbath", "black")]
# elif len(argv) == 2:
# players = [AgentPlayer(argv[1], "white"), HumanPlayer("shadow", "black"), ]
# elif len(argv) == 3:
# players = [AgentPlayer(argv[1], "white"), AgentPlayer(argv[2], "black")]
board = Board(style = "quantum")
# Construct the board!
if len(argv) == 1:
players = [NetworkSender(HumanPlayer("saruman", "white"), board), NetworkReceiver("black", board, 'localhost')]
else:
players = [NetworkReceiver("white", board, 'localhost'), NetworkSender(HumanPlayer("sabbath", "black"), board)]
graphics = GraphicsThread(board, grid_sz = [64,64]) # Construct a GraphicsThread! I KNOW WHAT I'M DOING! BEAR WITH ME!
global turn_delay
global agent_timeout
global log_file
global src_file
game = GameThread(board, players) # Construct a GameThread! Make it global! Damn the consequences!
game.start() # This runs in a new thread
style = "quantum"
colour = "white"
graphics_enabled = True
players = []
i = 0
while i < len(argv)-1:
i += 1
arg = argv[i]
if arg[0] != '-':
players.append(make_player(arg, colour))
if colour == "white":
colour = "black"
elif colour == "black":
pass
else:
sys.stderr.write(sys.argv[0] + " : Too many players (max 2)\n")
continue
# Option parsing goes here
if arg[1] == '-' and arg[2:] == "classical":
style = "classical"
elif arg[1] == '-' and arg[2:] == "quantum":
style = "quantum"
elif (arg[1] == '-' and arg[2:] == "graphics"):
graphics_enabled = not graphics_enabled
elif (arg[1] == '-' and arg[2:].split("=")[0] == "file"):
# Load game from file
if len(arg[2:].split("=")) == 1:
src_file = sys.stdout
else:
src_file = arg[2:].split("=")[1]
elif (arg[1] == '-' and arg[2:].split("=")[0] == "log"):
# Log file
if len(arg[2:].split("=")) == 1:
log_file = sys.stdout
else:
log_file = arg[2:].split("=")[1]
elif (arg[1] == '-' and arg[2:].split("=")[0] == "delay"):
# Delay
if len(arg[2:].split("=")) == 1:
turn_delay = 0
else:
turn_delay = float(arg[2:].split("=")[1])
elif (arg[1] == '-' and arg[2:].split("=")[0] == "timeout"):
# Timeout
if len(arg[2:].split("=")) == 1:
agent_timeout = -1
elif platform.system() != "Windows": # Windows breaks this option
agent_timeout = float(arg[2:].split("=")[1])
else:
sys.stderr.write(sys.argv[0] + " : Warning - You are using Windows\n")
agent_timeout = -1
elif (arg[1] == '-' and arg[2:] == "help"):
# Help
os.system("less data/help.txt") # The best help function
return 0
# Create the board
board = Board(style)
# Initialise GUI
if graphics_enabled == True:
try:
graphics = GraphicsThread(board, grid_sz = [64,64]) # Construct a GraphicsThread!
except Exception,e:
graphics = None
sys.stderr.write(sys.argv[0] + " : Got exception trying to initialise graphics\n"+str(e.message)+"\nDisabled graphics\n")
graphics_enabled = False
# If there are no players listed, display a nice pretty menu
if len(players) != 2:
if graphics != None:
players = graphics.SelectPlayers(players)
else:
sys.stderr.write(sys.argv[0] + " : Usage " + sys.argv[0] + " white black\n")
return 44
# If there are still no players, quit
if players == None or len(players) != 2:
sys.stderr.write(sys.argv[0] + " : Graphics window closed before players chosen\n")
return 45
# Wrap NetworkSender players around original players if necessary
for i in range(len(players)):
if isinstance(players[i], NetworkReceiver):
players[i].board = board # Network players need direct access to the board
for j in range(len(players)):
if j == i:
continue
if isinstance(players[j], NetworkSender) or isinstance(players[j], NetworkReceiver):
continue
players[j] = NetworkSender(players[j], players[i].address)
players[j].board = board
# Connect the networked players
for p in players:
if isinstance(p, NetworkSender) or isinstance(p, NetworkReceiver):
if graphics != None:
graphics.board.display_grid(graphics.window, graphics.grid_sz)
graphics.message("Connecting to " + p.colour + " player...")
p.connect()
# Construct a GameThread! Make it global! Damn the consequences!
game = GameThread(board, players)
graphics.run()
game.join()
return game.error + graphics.error
if graphics != None:
game.start() # This runs in a new thread
graphics.run()
game.join()
return game.error + graphics.error
else:
game.run()
return game.error
# This is how python does a main() function...
if __name__ == "__main__":
......
import socket
import select
network_timeout_start = -1.0 # Timeout in seconds to wait for the start of a message
network_timeout_delay = 1.0 # Maximum time between two characters being received
class Network():
def __init__(self, colour, address = None):
self.socket = socket.socket()
#self.socket.setblocking(0)
if colour == "white":
self.port = 4563
self.port = 4562
else:
self.port = 4564
self.port = 4563
self.src = None
# print str(self) + " listens on port " + str(self.port)
if address == None:
self.host = 'localhost' #socket.gethostname()
self.host = socket.gethostname()
self.socket.bind((self.host, self.port))
self.socket.listen(5)
self.src, self.address = self.socket.accept()
self.src.send("ok\n")
if self.get_response() == "QUIT":
self.src.close()
else:
self.host = address
self.socket.connect(('localhost', self.port))
self.socket.connect((address, self.port))
self.src = self.socket
self.src.send("ok\n")
if self.get_response() == "QUIT":
self.src.close()
def get_response(self):
# Timeout the start of the message (first character)
if network_timeout_start > 0.0:
ready = select.select([self.src], [], [], network_timeout_start)[0]
else:
ready = [self.src]
if self.src in ready:
s = self.src.recv(1)
else:
raise Exception("UNRESPONSIVE")
def getline(self):
s = self.src.recv(1)
while s[len(s)-1] != '\n':
s += self.src.recv(1)
return s
# Timeout on each character in the message
if network_timeout_delay > 0.0:
ready = select.select([self.src], [], [], network_timeout_delay)[0]
else:
ready = [self.src]
if self.src in ready:
s += self.src.recv(1)