Commit 65d9bb84 authored by Sam Moore's avatar Sam Moore

Initial Commit

Now is probably a good time to start using git

1. Implemented quantum chess as described here:
        http://research.cs.queensu.ca/Parallel/QuantumChess/QuantumChess.html
	- Except I have "white on the right"

2. Wrote sample agent "agent_bishop.py" which is already better at the game than I am

3. Sort of got sidetracked trying to split my original quantum chess python file into multiple files.
        - Difficulties because there are circular dependencies with the graphics / game stuff
        - eg: HumanPlayer needs to know about GraphicsThread to get its move
                and GraphicsThread needs to know about HumanPlayer to draw stuff and give it a move

4. Ended up writing a bash script to combine multiple python files into single qchess.py file.
        - It was easier than working out how __init__.py and __main__.py work / do not work
        - "from . import *" doesn't work and apparently is "bad practice" or something
        - Bash scripts are the best practice
        - It will probably backfire horribly. Hence, git

TODO:
        - Either borrow sigma from [DJA] again, or setup a new progcomp server at UCC
	- Make website for results / information
		- Depending upon how adventurous I feel it might be django
		- Or a .html file created by a bash script
	- Make judging / scoring system
		- Need to implement move timeouts for AI players (remember to use select this time)
	- Organise some sort of event for people to come to
	- Should probably talk to #committee at some point
	- Get people interested? Bribe them? Threaten them with account locking if they don't enter?
		- If I get 4 entrants it will be a doubling in entries since 2010!
parents
#!/usr/bin/python -u
from qchess import *
"""
Agent Bishop
( an agent, not an implementation of a bishop chess piece!)
"""
# Skeleton class for your agent
class Agent(AgentRandom): # Inherits from AgentRandom (in qchess.py)
def __init__(self, name, colour):
AgentRandom.__init__(self, name, colour)
self.value = {"pawn" : 1, "bishop" : 3, "knight" : 3, "rook" : 5, "queen" : 9, "king" : 100, "unknown" : 4}
self.aggression = 2.0 # Multiplier for scoring due to aggressive actions
self.defence = 1.0 # Multiplier for scoring due to defensive actions
self.depth = 0 # Current depth
self.max_depth = 2 # Recurse this many times (for some reason, makes more mistakes when this is increased???)
self.recurse_for = -1 # Recurse for the best few moves each times (less than 0 = all moves)
for p in self.board.pieces["white"] + self.board.pieces["black"]:
p.last_moves = None
p.selected_moves = None
def get_value(self, piece):
if piece == None:
return 0.0
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")
grid = self.board.probability_grid(piece)
#sys.stderr.write("\t Probability grid " + str(grid) + "\n")
moves = []
for x in range(w):
for y in range(h):
if grid[x][y] < 0.3: # Throw out moves with < 30% probability
#sys.stderr.write("\tReject " + str(x) + "," + str(y) + " (" + str(grid[x][y]) + ")\n")
continue
target = self.board.grid[x][y]
# Get total probability that the move is protected
[xx,yy] = [piece.x, piece.y]
[piece.x, piece.y] = [x, y]
self.board.grid[x][y] = piece
self.board.grid[xx][yy] = None
defenders = self.board.coverage(x, y, piece.colour, reject_allied = False)
d_prob = 0.0
for d in defenders.keys():
d_prob += defenders[d]
if len(defenders.keys()) > 0:
d_prob /= float(len(defenders.keys()))
if (d_prob > 1.0):
d_prob = 1.0
# Get total probability that the move is threatened
attackers = self.board.coverage(x, y, opponent(piece.colour), reject_allied = False)
a_prob = 0.0
for a in attackers.keys():
a_prob += attackers[a]
if len(attackers.keys()) > 0:
a_prob /= float(len(attackers.keys()))
if (a_prob > 1.0):
a_prob = 1.0
self.board.grid[x][y] = target
self.board.grid[xx][yy] = piece
[piece.x, piece.y] = [xx, yy]
# Score of the move
value = self.aggression * (1.0 + d_prob) * self.get_value(target) - self.defence * (1.0 - d_prob) * a_prob * self.get_value(piece)
# Adjust score based on movement of piece out of danger
attackers = self.board.coverage(piece.x, piece.y, opponent(piece.colour))
s_prob = 0.0
for a in attackers.keys():
s_prob += attackers[a]
if len(attackers.keys()) > 0:
s_prob /= float(len(attackers.keys()))
if (s_prob > 1.0):
s_prob = 1.0
value += self.defence * s_prob * self.get_value(piece)
# Adjust score based on probability that the move is actually possible
moves.append([[x, y], grid[x][y] * value])
moves.sort(key = lambda e : e[1], reverse = True)
#sys.stderr.write(sys.argv[0] + ": Moves for " + str(piece) + " are " + str(moves) + "\n")
piece.last_moves = moves
piece.selected_moves = None
return moves
def select_best(self, colour):
self.depth += 1
all_moves = {}
for p in self.board.pieces[colour]:
self.choice = p # Temporarily pick that piece
m = self.prioritise_moves(p)
if len(m) > 0:
all_moves.update({p : m[0]})
if len(all_moves.items()) <= 0:
return None
opts = all_moves.items()
opts.sort(key = lambda e : e[1][1], reverse = True)
if self.depth >= self.max_depth:
self.depth -= 1
return list(opts[0])
if self.recurse_for >= 0:
opts = opts[0:self.recurse_for]
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]:
[xx,yy] = [choice[0].x, choice[0].y] # Remember position
[nx,ny] = choice[1][0] # Target
[choice[0].x, choice[0].y] = [nx, ny] # Set position
target = self.board.grid[nx][ny] # Remember piece in spot
self.board.grid[xx][yy] = None # Remove piece
self.board.grid[nx][ny] = choice[0] # Replace with moving piece
# Recurse
best_enemy_move = self.select_best(opponent(choice[0].colour))
choice[1][1] -= best_enemy_move[1][1] / float(self.depth + 1.0)
[choice[0].x, choice[0].y] = [xx, yy] # Restore position
self.board.grid[nx][ny] = target # Restore taken piece
self.board.grid[xx][yy] = choice[0] # Restore moved piece
opts.sort(key = lambda e : e[1][1], reverse = True)
sys.stderr.write(sys.argv[0] + " : After recurse, options are " + str(opts) + "\n")
self.depth -= 1
return list(opts[0])
# Returns [x,y] of selected piece
def select(self):
self.choice = self.select_best(self.colour)[0]
return [self.choice.x, self.choice.y]
# Returns [x,y] of square to move selected piece into
def get_move(self):
self.choice.selected_moves = self.choice.last_moves
moves = self.prioritise_moves(self.choice)
if len(moves) > 0:
return moves[0][0]
else:
return AgentRandom.get_move(self)
# Horrible messy graphics class that draws what the agent is doing, kind of useful for testing
class AgentGraphics(GraphicsThread):
def __init__(self, board, title):
GraphicsThread.__init__(self, board, title, grid_sz = [64,64])
self.choice = None
self.moves = None
def run(self):
square_img = pygame.Surface((self.grid_sz[0], self.grid_sz[1]),pygame.SRCALPHA) # A square image
while not self.stopped():
self.board.display_grid(window = self.window, grid_sz = self.grid_sz)
# Draw choice of the AI
if agent.choice != None:
mp = [self.grid_sz[i] * [agent.choice.x, agent.choice.y][i] for i in range(2)]
square_img.fill(pygame.Color(0,255,0,64))
self.window.blit(square_img, mp)
# Draw calculated choices for the piece clicked on
if self.choice != None:
mp = [self.grid_sz[i] * [self.choice.x, self.choice.y][i] for i in range(2)]
square_img.fill(pygame.Color(0,0,255,128))
self.window.blit(square_img, mp)
# Draw the choices the AI calculated from the selection of the chosen piece
if agent.choice != None and agent.choice.selected_moves != None:
for m in agent.choice.selected_moves:
mp = [m[0][i] * self.grid_sz[i] for i in range(2)]
square_img.fill(pygame.Color(128,128,255,128))
self.window.blit(square_img, mp)
font = pygame.font.Font(None, 14)
text = font.render("{0:.2f}".format(round(m[1],2)), 1, pygame.Color(255,0,0))
mp[0] = mp[0] + self.grid_sz[0] - text.get_width()
mp[1] = mp[1] + self.grid_sz[1] - text.get_height()
self.window.blit(text, mp)
# Draw the choice the AI's chosen piece could have actually made
if agent.choice != None and agent.choice.last_moves != None:
for m in agent.choice.last_moves:
mp = [m[0][i] * self.grid_sz[i] for i in range(2)]
square_img.fill(pygame.Color(255,0,0,128))
self.window.blit(square_img, mp)
font = pygame.font.Font(None, 14)
text = font.render("{0:.2f}".format(round(m[1],2)), 1, pygame.Color(0,0,255))
mp[0] = mp[0] + self.grid_sz[0] - text.get_width()
self.window.blit(text, mp)
if self.moves != None:
for m in self.moves:
mp = [m[0][i] * self.grid_sz[i] for i in range(2)]
square_img.fill(pygame.Color(255,0,255,128))
self.window.blit(square_img, mp)
font = pygame.font.Font(None, 14)
text = font.render("{0:.2f}".format(round(m[1],2)), 1, pygame.Color(0,0,0))
self.window.blit(text, mp)
self.board.display_pieces(window = self.window, grid_sz = self.grid_sz)
pygame.display.flip()
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.stop()
break
elif event.type == pygame.MOUSEBUTTONDOWN:
m = [event.pos[i] / self.grid_sz[i] for i in range(len(event.pos))]
p = agent.board.grid[m[0]][m[1]]
if p == None:
continue
self.choice = p
self.last_moves = self.choice.last_moves
self.selected_moves = self.choice.selected_moves
if event.button == 3 or self.choice.last_moves == None:
self.moves = agent.prioritise_moves(self.choice)
else:
self.moves = self.choice.last_moves
elif event.type == pygame.MOUSEBUTTONUP:
if self.choice == None:
continue
self.choice.last_moves = self.last_moves
self.choice.selected_moves = self.selected_moves
self.choice = None
self.moves = None
pygame.display.quit()
# Main function; don't alter
def main(argv):
global agent
colour = sys.stdin.readline().strip("\n") # Gets the colour of the agent from stdin
agent = Agent(argv[0], colour) # Creates your agent
graphics = AgentGraphics(agent.board, title="Agent Bishop (" + str(colour) + ") - DEBUG VIEW")
graphics.start()
# Plays quantum chess using your agent
while True:
line = sys.stdin.readline().strip(" \r\n")
#sys.stderr.write(argv[0] + ": gets line \"" + str(line) + "\"\n")
if line == "SELECTION?":
[x,y] = agent.select() # Gets your agent's selection
#print "Select " + str(x) + "," + str(y)
sys.stdout.write(str(x) + " " + str(y) + "\n")
elif line == "MOVE?":
[x,y] = agent.get_move() # Gets your agent's move
sys.stdout.write(str(x) + " " + str(y) + "\n")
elif line.split(" ")[0] == "QUIT":
agent.quit(" ".join(line.split(" ")[1:])) # Quits the game
# graphics.stop()
break
else:
agent.update(line) # Updates agent.board
graphics.stop()
graphics.join()
return 0
# Don't touch this
if __name__ == "__main__":
sys.exit(main(sys.argv))
../data/
\ No newline at end of file
../qchess.py
\ No newline at end of file
[w,h] = [8,8] # Width and height of board(s)
# Class to represent a quantum chess board
class Board():
# Initialise; if master=True then the secondary piece types are assigned
# Otherwise, they are left as unknown
# So you can use this class in Agent programs, and fill in the types as they are revealed
def __init__(self, style="agent"):
self.style = style
self.pieces = {"white" : [], "black" : []}
self.grid = [[None] * w for _ in range(h)] # 2D List (you can get arrays in python, somehow, but they scare me)
self.unrevealed_types = {"white" : piece_types.copy(), "black" : piece_types.copy()}
self.king = {"white" : None, "black" : None} # We need to keep track of the king, because he is important
for c in ["black", "white"]:
del self.unrevealed_types[c]["unknown"]
# Add all the pieces with known primary types
for i in range(0, 2):
s = ["black", "white"][i]
c = self.pieces[s]
y = [0, h-1][i]
c.append(Piece(s, 0, y, ["rook"]))
c.append(Piece(s, 1, y, ["knight"]))
c.append(Piece(s, 2, y, ["bishop"]))
k = Piece(s, 3, y, ["king", "king"]) # There can only be one ruler!
k.types_revealed[1] = True
k.current_type = "king"
self.king[s] = k
c.append(k)
c.append(Piece(s, 4, y, ["queen"])) # Apparently he may have multiple wives though.
c.append(Piece(s, 5, y, ["bishop"]))
c.append(Piece(s, 6, y, ["knight"]))
c.append(Piece(s, 7, y, ["rook"]))
if y == 0:
y += 1
else:
y -= 1
# Lots of pawn
for x in range(0, w):
c.append(Piece(s, x, y, ["pawn"]))
types_left = {}
types_left.update(piece_types)
del types_left["king"] # We don't want one of these randomly appearing (although it might make things interesting...)
del types_left["unknown"] # We certainly don't want these!
for piece in c:
# Add to grid
self.grid[piece.x][piece.y] = piece
if len(piece.types) > 1:
continue
if style == "agent": # Assign placeholder "unknown" secondary type
piece.types.append("unknown")
continue
elif style == "quantum":
# The master allocates the secondary types
choice = types_left.keys()[random.randint(0, len(types_left.keys())-1)]
types_left[choice] -= 1
if types_left[choice] <= 0:
del types_left[choice]
piece.types.append(choice)
elif style == "classical":
piece.types.append(piece.types[0])
piece.current_type = piece.types[0]
piece.types_revealed[1] = True
piece.choice = 0
def clone(self):
newboard = Board(master = False)
newpieces = newboard.pieces["white"] + newboard.pieces["black"]
mypieces = self.pieces["white"] + self.pieces["black"]
for i in range(len(mypieces)):
newpieces[i].init_from_copy(mypieces[i])
def display_grid(self, window = None, grid_sz = [80,80]):
if window == None:
return # I was considering implementing a text only display, then I thought "Fuck that"
# The indentation is getting seriously out of hand...
for x in range(0, w):
for y in range(0, h):
if (x + y) % 2 == 0:
c = pygame.Color(200,200,200)
else:
c = pygame.Color(64,64,64)
pygame.draw.rect(window, c, (x*grid_sz[0], y*grid_sz[1], (x+1)*grid_sz[0], (y+1)*grid_sz[1]))
def display_pieces(self, window = None, grid_sz = [80,80]):
if window == None:
return
for p in self.pieces["white"] + self.pieces["black"]:
p.draw(window, grid_sz)
# Draw the board in a pygame window
def display(self, window = None):
self.display_grid(window)
self.display_pieces(window)
def verify(self):
for x in range(w):
for y in range(h):
if self.grid[x][y] == None:
continue
if (self.grid[x][y].x != x or self.grid[x][y].y != y):
raise Exception(sys.argv[0] + ": MISMATCH " + str(self.grid[x][y]) + " should be at " + str(x) + "," + str(y))
# Select a piece on the board (colour is the colour of whoever is doing the selecting)
def select(self, x,y, colour=None):
if not self.on_board(x, y): # Get on board everyone!
raise Exception("BOUNDS")
piece = self.grid[x][y]
if piece == None:
raise Exception("EMPTY")
if colour != None and piece.colour != colour:
raise Exception("COLOUR")
# I'm not quite sure why I made this return a string, but screw logical design
return str(x) + " " + str(y) + " " + str(piece.select()) + " " + str(piece.current_type)
# Update the board when a piece has been selected
# "type" is apparently reserved, so I'll use "state"
def update_select(self, x, y, type_index, state):
piece = self.grid[x][y]
if piece.types[type_index] == "unknown":
if not state in self.unrevealed_types[piece.colour].keys():
raise Exception("SANITY: Too many " + piece.colour + " " + state + "s")
self.unrevealed_types[piece.colour][state] -= 1
if self.unrevealed_types[piece.colour][state] <= 0:
del self.unrevealed_types[piece.colour][state]
piece.types[type_index] = state
piece.types_revealed[type_index] = True
piece.current_type = state
if len(self.possible_moves(piece)) <= 0:
piece.deselect() # Piece can't move; deselect it
# Update the board when a piece has been moved
def update_move(self, x, y, x2, y2):
piece = self.grid[x][y]
self.grid[x][y] = None
taken = self.grid[x2][y2]
if taken != None:
if taken.current_type == "king":
self.king[taken.colour] = None
self.pieces[taken.colour].remove(taken)
self.grid[x2][y2] = piece
piece.x = x2
piece.y = y2
# If the piece is a pawn, and it reaches the final row, it becomes a queen
# I know you are supposed to get a choice
# But that would be effort
if piece.current_type == "pawn" and ((piece.colour == "white" and piece.y == 0) or (piece.colour == "black" and piece.y == h-1)):
if self.style == "classical":
piece.types[0] = "queen"
piece.types[1] = "queen"
else:
piece.types[piece.choice] = "queen"
piece.current_type = "queen"
piece.deselect() # Uncollapse (?) the wavefunction!
self.verify()
# Update the board from a string
# Guesses what to do based on the format of the string
def update(self, result):
#print "Update called with \"" + str(result) + "\""
# String always starts with 'x y'
try:
s = result.split(" ")
[x,y] = map(int, s[0:2])
except:
raise Exception("GIBBERISH \""+ str(result) + "\"") # Raise expectations
piece = self.grid[x][y]
if piece == None:
raise Exception("EMPTY")
# If a piece is being moved, the third token is '->'
# We could get away with just using four integers, but that wouldn't look as cool
if "->" in s:
# Last two tokens are the destination
try:
[x2,y2] = map(int, s[3:])
except:
raise Exception("GIBBERISH \"" + str(result) + "\"") # Raise the alarm
# Move the piece (take opponent if possible)
self.update_move(x, y, x2, y2)
else:
# Otherwise we will just assume a piece has been selected
try:
type_index = int(s[2]) # We need to know which of the two types the piece is in; that's the third token
state = s[3] # The last token is a string identifying the type
except:
raise Exception("GIBBERISH \"" + result + "\"") # Throw a hissy fit
# Select the piece
self.update_select(x, y, type_index, state)
return result
# Gets each piece that could reach the given square and the probability that it could reach that square
# Will include allied pieces that defend the attacker
def coverage(self, x, y, colour = None, reject_allied = True):
result = {}
if colour == None:
pieces = self.pieces["white"] + self.pieces["black"]
else:
pieces = self.pieces[colour]
for p in pieces:
prob = self.probability_grid(p, reject_allied)[x][y]
if prob > 0:
result.update({p : prob})
self.verify()
return result
# Associates each square with a probability that the piece could move into it
# Look, I'm doing all the hard work for you here...
def probability_grid(self, p, reject_allied = True):
result = [[0.0] * w for _ in range(h)]
if not isinstance(p, Piece):
return result
if p.current_type != "unknown":
#sys.stderr.write(sys.argv[0] + ": " + str(p) + " moves " + str(self.possible_moves(p, reject_allied)) + "\n")
for point in self.possible_moves(p, reject_allied):
result[point[0]][point[1]] = 1.0
return result
for i in range(len(p.types)):
t = p.types[i]
prob = 0.5
if t == "unknown" or p.types_revealed[i] == False:
total_types = 0
for t2 in self.unrevealed_types[p.colour].keys():
total_types += self.unrevealed_types[p.colour][t2]
for t2 in self.unrevealed_types[p.colour].keys():
prob2 = float(self.unrevealed_types[p.colour][t2]) / float(total_types)
p.current_type = t2
for point in self.possible_moves(p, reject_allied):
result[point[0]][point[1]] += prob2 * prob
else:
p.current_type = t
for point in self