Final Commit
[progcomp2013.git] / qchess / src / board.py
1 [w,h] = [8,8] # Width and height of board(s)
2
3 always_reveal_states = False
4
5 # Class to represent a quantum chess board
6 class Board():
7         # Initialise; if master=True then the secondary piece types are assigned
8         #       Otherwise, they are left as unknown
9         #       So you can use this class in Agent programs, and fill in the types as they are revealed
10         def __init__(self, style="agent"):
11                 self.style = style
12                 self.pieces = {"white" : [], "black" : []}
13                 self.grid = [[None] * w for _ in range(h)] # 2D List (you can get arrays in python, somehow, but they scare me)
14                 self.unrevealed_types = {"white" : piece_types.copy(), "black" : piece_types.copy()}
15                 self.king = {"white" : None, "black" : None} # We need to keep track of the king, because he is important
16                 self.max_moves = None
17                 self.moves = 0
18                 self.move_stack = []
19                 for c in ["black", "white"]:
20                         del self.unrevealed_types[c]["unknown"]
21
22                 if style == "empty":
23                         return
24
25                 # Add all the pieces with known primary types
26                 for i in range(0, 2):
27                         
28                         s = ["black", "white"][i]
29                         c = self.pieces[s]
30                         y = [0, h-1][i]
31
32                         c.append(Piece(s, 0, y, ["rook"]))
33                         c.append(Piece(s, 1, y, ["knight"]))
34                         c.append(Piece(s, 2, y, ["bishop"]))
35                         k = Piece(s, 3, y, ["king", "king"]) # There can only be one ruler!
36                         k.current_type = "king"
37                         self.king[s] = k
38                         c.append(k)
39                         c.append(Piece(s, 4, y, ["queen"])) # Apparently he may have multiple wives though.
40                         c.append(Piece(s, 5, y, ["bishop"]))
41                         c.append(Piece(s, 6, y, ["knight"]))
42                         c.append(Piece(s, 7, y, ["rook"]))
43                         
44                         if y == 0: 
45                                 y += 1 
46                         else: 
47                                 y -= 1
48                         
49                         # Lots of pawn
50                         for x in range(0, w):
51                                 c.append(Piece(s, x, y, ["pawn"]))
52
53                         types_left = {}
54                         types_left.update(piece_types)
55                         del types_left["king"] # We don't want one of these randomly appearing (although it might make things interesting...)
56                         del types_left["unknown"] # We certainly don't want these!
57                         for piece in c:
58                                 # Add to grid
59                                 self.grid[piece.x][piece.y] = piece 
60
61                                 if len(piece.types) > 1:
62                                         continue                                
63                                 if style == "agent": # Assign placeholder "unknown" secondary type
64                                         piece.types.append("unknown")
65                                         continue
66
67                                 elif style == "quantum":
68                                         # The master allocates the secondary types
69                                         choice = types_left.keys()[random.randint(0, len(types_left.keys())-1)]
70                                         types_left[choice] -= 1
71                                         if types_left[choice] <= 0:
72                                                 del types_left[choice]
73                                         piece.types.append('?' + choice)
74                                 elif style == "classical":
75                                         piece.types.append(piece.types[0])
76                                         piece.current_type = piece.types[0]
77                                         piece.choice = 0
78
79         def clone(self):
80                 newboard = Board(master = False)
81                 newpieces = newboard.pieces["white"] + newboard.pieces["black"]
82                 mypieces = self.pieces["white"] + self.pieces["black"]
83
84                 for i in range(len(mypieces)):
85                         newpieces[i].init_from_copy(mypieces[i])
86         
87         # Reset the board from a string
88         def reset_board(self, s):
89                 self.pieces = {"white" : [], "black" : []}
90                 self.king = {"white" : None, "black" : None}
91                 self.grid = [[None] * w for _ in range(h)]
92                 for x in range(w):
93                         for y in range(h):
94                                 self.grid[x][y] = None
95
96                 for line in s.split("\n"):
97                         if line == "":
98                                 continue
99                         if line[0] == "#":
100                                 continue
101
102                         tokens = line.split(" ")
103                         [x, y] = map(int, tokens[len(tokens)-1].split(","))
104                         current_type = tokens[1]
105                         types = map(lambda e : e.strip(" '[],"), line.split('[')[1].split(']')[0].split(','))
106                         
107                         target = Piece(tokens[0], x, y, types)
108                         target.current_type = current_type
109                         
110                         try:
111                                 target.choice = types.index(current_type)
112                         except:
113                                 target.choice = -1
114
115                         self.pieces[tokens[0]].append(target)
116                         if target.current_type == "king":
117                                 self.king[tokens[0]] = target
118
119                         self.grid[x][y] = target
120                         
121
122         def display_grid(self, window = None, grid_sz = [80,80]):
123                 if window == None:
124                         return # I was considering implementing a text only display, then I thought "Fuck that"
125
126                 # The indentation is getting seriously out of hand...
127                 for x in range(0, w):
128                         for y in range(0, h):
129                                 if (x + y) % 2 == 0:
130                                         c = pygame.Color(200,200,200)
131                                 else:
132                                         c = pygame.Color(64,64,64)
133                                 pygame.draw.rect(window, c, (x*grid_sz[0], y*grid_sz[1], (x+1)*grid_sz[0], (y+1)*grid_sz[1]))
134
135         def display_pieces(self, window = None, grid_sz = [80,80]):
136                 if window == None:
137                         return
138                 for p in self.pieces["white"] + self.pieces["black"]:
139                         p.draw(window, grid_sz, self.style)
140
141         # Draw the board in a pygame window
142         def display(self, window = None):
143                 self.display_grid(window)
144                 self.display_pieces(window)
145                 
146
147                 
148
149         def verify(self):
150                 for x in range(w):
151                         for y in range(h):
152                                 if self.grid[x][y] == None:
153                                         continue
154                                 if (self.grid[x][y].x != x or self.grid[x][y].y != y):
155                                         raise Exception(sys.argv[0] + ": MISMATCH " + str(self.grid[x][y]) + " should be at " + str(x) + "," + str(y))
156
157         # Select a piece on the board (colour is the colour of whoever is doing the selecting)
158         def select(self, x,y, colour=None):
159                 if not self.on_board(x, y): # Get on board everyone!
160                         raise Exception("BOUNDS " + str(x) + ","+str(y))
161
162                 piece = self.grid[x][y]
163                 if piece == None:
164                         raise Exception("EMPTY")
165
166                 if colour != None and piece.colour != colour:
167                         raise Exception("COLOUR " + str(piece.colour) + " not " + str(colour))
168
169                 # I'm not quite sure why I made this return a string, but screw logical design
170                 return str(x) + " " + str(y) + " " + str(piece.select()) + " " + str(piece.current_type)
171
172
173         # Update the board when a piece has been selected
174         # "type" is apparently reserved, so I'll use "state"
175         def update_select(self, x, y, type_index, state, sanity=True, deselect=True):
176                 #debug(str(self) + " update_select called")
177                 piece = self.grid[x][y]
178                 if piece.types[type_index] == "unknown":
179                         if not state in self.unrevealed_types[piece.colour].keys() and sanity == True:
180                                 raise Exception("SANITY: Too many " + piece.colour + " " + state + "s")
181                         self.unrevealed_types[piece.colour][state] -= 1
182                         if self.unrevealed_types[piece.colour][state] <= 0:
183                                 del self.unrevealed_types[piece.colour][state]
184
185                 piece.types[type_index] = state
186                 piece.current_type = state
187
188                 if deselect == True and len(self.possible_moves(piece)) <= 0:
189                         piece.deselect() # Piece can't move; deselect it
190                         
191                 # Piece needs to recalculate moves
192                 piece.possible_moves = None
193                 
194         # Update the board when a piece has been moved
195         def update_move(self, x, y, x2, y2, sanity=True):
196                 #debug(str(self) + " update_move called \""+str(x)+ " " + str(y) + " -> " + str(x2) + " " + str(y2) + "\"")     
197                 piece = self.grid[x][y]
198                 #print "Moving " + str(x) + "," + str(y) + " to " + str(x2) + "," + str(y2) + "; possible_moves are " + str(self.possible_moves(piece))
199                 
200                 if not [x2,y2] in self.possible_moves(piece) and sanity == True:
201                         raise Exception("ILLEGAL move " + str(x2)+","+str(y2))
202                 
203                 self.grid[x][y] = None
204                 taken = self.grid[x2][y2]
205                 if taken != None:
206                         if taken.current_type == "king":
207                                 self.king[taken.colour] = None
208                         self.pieces[taken.colour].remove(taken)
209                 self.grid[x2][y2] = piece
210                 piece.x = x2
211                 piece.y = y2
212
213                 # If the piece is a pawn, and it reaches the final row, it becomes a queen
214                 # I know you are supposed to get a choice
215                 # But that would be effort
216                 if piece.current_type == "pawn" and ((piece.colour == "white" and piece.y == 0) or (piece.colour == "black" and piece.y == h-1)):
217                         if self.style == "classical":
218                                 piece.types[0] = "queen"
219                                 piece.types[1] = "queen"
220                         else:
221                                 piece.types[piece.choice] = "queen"
222                         piece.current_type = "queen"
223
224                 piece.deselect() # Uncollapse (?) the wavefunction!
225                 self.moves += 1
226                 
227                 # All other pieces need to recalculate moves
228                 for p in self.pieces["white"] + self.pieces["black"]:
229                         p.possible_moves = None
230                 
231                 #self.verify()  
232
233         # Update the board from a string
234         # Guesses what to do based on the format of the string
235         def update(self, result, sanity=True, deselect=True):
236                 #debug(str(self) + " update called \""+str(result)+"\"")
237                 # String always starts with 'x y'
238                 try:
239                         s = result.split(" ")
240                         [x,y] = map(int, s[0:2])        
241                 except:
242                         raise Exception("GIBBERISH \""+ str(result) + "\"") # Raise expectations
243
244                 piece = self.grid[x][y]
245                 if piece == None and sanity == True:
246                         raise Exception("EMPTY " + str(x) + " " + str(y))
247
248                 # If a piece is being moved, the third token is '->'
249                 # We could get away with just using four integers, but that wouldn't look as cool
250                 if "->" in s:
251                         # Last two tokens are the destination
252                         try:
253                                 [x2,y2] = map(int, s[3:])
254                         except:
255                                 raise Exception("GIBBERISH \"" + str(result) + "\"") # Raise the alarm
256
257                         # Move the piece (take opponent if possible)
258                         self.update_move(x, y, x2, y2, sanity)
259                         
260                 else:
261                         # Otherwise we will just assume a piece has been selected
262                         try:
263                                 type_index = int(s[2]) # We need to know which of the two types the piece is in; that's the third token
264                                 state = s[3] # The last token is a string identifying the type
265                         except:
266                                 raise Exception("GIBBERISH \"" + result + "\"") # Throw a hissy fit
267
268
269                         # Select the piece
270                         self.update_select(x, y, type_index, state, sanity=sanity, deselect=deselect)
271
272                 return result
273
274         # Gets each piece that could reach the given square and the probability that it could reach that square 
275         # Will include allied pieces that defend the attacker
276         def coverage(self, x, y, colour = None, reject_allied = True):
277                 result = {}
278                 
279                 if colour == None:
280                         pieces = self.pieces["white"] + self.pieces["black"]
281                 else:
282                         pieces = self.pieces[colour]
283
284                 for p in pieces:
285                         prob = self.probability_grid(p, reject_allied)[x][y]
286                         if prob > 0:
287                                 result.update({p : prob})
288                 
289                 #self.verify()
290                 return result
291
292
293                 
294
295
296         # Associates each square with a probability that the piece could move into it
297         # Look, I'm doing all the hard work for you here...
298         def probability_grid(self, p, reject_allied = True):
299                 
300                 result = [[0.0] * w for _ in range(h)]
301                 if not isinstance(p, Piece):
302                         return result
303
304                 if p.current_type != "unknown":
305                         #sys.stderr.write(sys.argv[0] + ": " + str(p) + " moves " + str(self.possible_moves(p, reject_allied)) + "\n")
306                         for point in self.possible_moves(p, reject_allied):
307                                 result[point[0]][point[1]] = 1.0
308                         return result
309                 
310                 
311                 for i in range(len(p.types)):
312                         t = p.types[i]
313                         prob = 1.0 / float(len(p.types))
314                         if t == "unknown" or p.types[i][0] == '?':
315                                 total_types = 0
316                                 for t2 in self.unrevealed_types[p.colour].keys():
317                                         total_types += self.unrevealed_types[p.colour][t2]
318                                 
319                                 for t2 in self.unrevealed_types[p.colour].keys():
320                                         prob2 = float(self.unrevealed_types[p.colour][t2]) / float(total_types)
321                                         #p.current_type = t2
322                                         for point in self.possible_moves(p, reject_allied, state=t2):
323                                                 result[point[0]][point[1]] += prob2 * prob
324                                 
325                         else:
326                                 #p.current_type = t
327                                 for point in self.possible_moves(p, reject_allied, state=t):
328                                                 result[point[0]][point[1]] += prob
329                 
330                 #self.verify()
331                 #p.current_type = "unknown"
332                 return result
333
334         def prob_is_type(self, p, state):
335                 if p.current_type != 0:
336                         if state == p.current_type:
337                                 return 1.0
338                         else:
339                                 return 0.0
340                 
341                 prob = 0.5
342                 result = 0
343                 for i in range(len(p.types)):
344                         t = p.types[i]
345                         if t == state:
346                                 result += prob
347                                 continue        
348                         if t == "unknown" or p.types[i][0] == '?':
349                                 total_prob = 0
350                                 for t2 in self.unrevealed_types[p.colour].keys():
351                                         total_prob += self.unrevealed_types[p.colour][t2]
352                                 for t2 in self.unrevealed_types[p.colour].keys():
353                                         if t2 == state:
354                                                 result += prob * float(self.unrevealed_types[p.colour][t2]) / float(total_prob)
355                                 
356
357
358         # Get all squares that the piece could move into
359         # This is probably inefficient, but I looked at some sample chess games and they seem to actually do things this way
360         # reject_allied indicates whether squares occupied by allied pieces will be removed
361         # (set to false to check for defense)
362         def possible_moves(self, p, reject_allied = True, state=None):
363                 if p == None:
364                         raise Exception("SANITY: No piece")
365                 
366                 
367                 
368                 if state != None and state != p.current_type:
369                         old_type = p.current_type
370                         p.current_type = state
371                         result = self.possible_moves(p, reject_allied, state=None)
372                         p.current_type = old_type
373                         return result
374                 
375                 
376                 
377                 
378                 result = []
379                 
380
381                 
382                 if p.current_type == "unknown":
383                         raise Exception("SANITY: Unknown state for piece: "+str(p))
384                         # The below commented out code causes things to break badly
385                         #for t in p.types:
386                         #       if t == "unknown":
387                         #               continue
388                         #       p.current_type = t
389                         #       result += self.possible_moves(p)                                                
390                         #p.current_type = "unknown"
391                         #return result
392
393                 if p.current_type == "king":
394                         result = [[p.x-1,p.y],[p.x+1,p.y],[p.x,p.y-1],[p.x,p.y+1], [p.x-1,p.y-1],[p.x-1,p.y+1],[p.x+1,p.y-1],[p.x+1,p.y+1]]
395                 elif p.current_type == "queen":
396                         for d in [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]]:
397                                 result += self.scan(p.x, p.y, d[0], d[1])
398                 elif p.current_type == "bishop":
399                         for d in [[-1,-1],[-1,1],[1,-1],[1,1]]: # There's a reason why bishops move diagonally
400                                 result += self.scan(p.x, p.y, d[0], d[1])
401                 elif p.current_type == "rook":
402                         for d in [[-1,0],[1,0],[0,-1],[0,1]]:
403                                 result += self.scan(p.x, p.y, d[0], d[1])
404                 elif p.current_type == "knight":
405                         # I would use two lines, but I'm not sure how python likes that
406                         result = [[p.x-2, p.y-1], [p.x-2, p.y+1], [p.x+2, p.y-1], [p.x+2,p.y+1], [p.x-1,p.y-2], [p.x-1, p.y+2],[p.x+1,p.y-2],[p.x+1,p.y+2]]
407                 elif p.current_type == "pawn":
408                         if p.colour == "white":
409                                 
410                                 # Pawn can't move forward into occupied square
411                                 if self.on_board(p.x, p.y-1) and self.grid[p.x][p.y-1] == None:
412                                         result = [[p.x,p.y-1]]
413                                 for f in [[p.x-1,p.y-1],[p.x+1,p.y-1]]:
414                                         if not self.on_board(f[0], f[1]):
415                                                 continue
416                                         if self.grid[f[0]][f[1]] != None:  # Pawn can take diagonally
417                                                 result.append(f)
418                                 if p.y == h-2:
419                                         # Slightly embarrassing if the pawn jumps over someone on its first move...
420                                         if self.grid[p.x][p.y-1] == None and self.grid[p.x][p.y-2] == None:
421                                                 result.append([p.x, p.y-2])
422                         else:
423                                 # Vice versa for the black pawn
424                                 if self.on_board(p.x, p.y+1) and self.grid[p.x][p.y+1] == None:
425                                         result = [[p.x,p.y+1]]
426
427                                 for f in [[p.x-1,p.y+1],[p.x+1,p.y+1]]:
428                                         if not self.on_board(f[0], f[1]):
429                                                 continue
430                                         if self.grid[f[0]][f[1]] != None:
431                                                 #sys.stderr.write(sys.argv[0] + " : "+str(p) + " can take " + str(self.grid[f[0]][f[1]]) + "\n")
432                                                 result.append(f)
433                                 if p.y == 1:
434                                         if self.grid[p.x][p.y+1] == None and self.grid[p.x][p.y+2] == None:
435                                                 result.append([p.x, p.y+2])
436
437                         #sys.stderr.write(sys.argv[0] + " : possible_moves for " + str(p) + " " + str(result) + "\n")
438
439                 # Remove illegal moves
440                 # Note: The result[:] creates a copy of result, so that the result.remove calls don't fuck things up
441                 for point in result[:]: 
442
443                         if (point[0] < 0 or point[0] >= w) or (point[1] < 0 or point[1] >= h):
444                                 result.remove(point) # Remove locations outside the board
445                                 continue
446                         g = self.grid[point[0]][point[1]]
447                         
448                         if g != None and (g.colour == p.colour and reject_allied == True):
449                                 result.remove(point) # Remove allied pieces
450                 
451                 #self.verify()
452                 
453                 p.possible_moves = result
454                 return result
455
456
457         # Scans in a direction until it hits a piece, returns all squares in the line
458         # (includes the final square (which contains a piece), but not the original square)
459         def scan(self, x, y, vx, vy):
460                 p = []
461                         
462                 xx = x
463                 yy = y
464                 while True:
465                         xx += vx
466                         yy += vy
467                         if not self.on_board(xx, yy):
468                                 break
469                         if not [xx,yy] in p:
470                                 p.append([xx, yy])
471                         g = self.grid[xx][yy]
472                         if g != None:
473                                 return p        
474                                         
475                 return p
476
477         # Returns "white", "black" or "DRAW" if the game should end
478         def end_condition(self):
479                 if self.king["white"] == None:
480                         if self.king["black"] == None:
481                                 return "DRAW" # This shouldn't happen
482                         return "black"
483                 elif self.king["black"] == None:
484                         return "white"
485                 elif len(self.pieces["white"]) == 1 and len(self.pieces["black"]) == 1:
486                         return "DRAW"
487                 elif self.max_moves != None and self.moves > self.max_moves:
488                         return "DRAW"
489                 return None
490
491
492         # I typed the full statement about 30 times before writing this function...
493         def on_board(self, x, y):
494                 return (x >= 0 and x < w) and (y >= 0 and y < h)
495         
496         
497         
498         # Pushes a move temporarily
499         def push_move(self, piece, x, y):
500                 target = self.grid[x][y]
501                 self.move_stack.append([piece, target, piece.x, piece.y, x, y])
502                 [piece.x, piece.y] = [x, y]
503                 self.grid[x][y] = piece
504                 self.grid[piece.x][piece.y] = None
505                 
506                 for p in self.pieces["white"] + self.pieces["black"]:
507                         p.possible_moves = None
508                 
509         # Restore move
510         def pop_move(self):
511                 #print str(self.move_stack)
512                 [piece, target, x1, y1, x2, y2] = self.move_stack[len(self.move_stack)-1]
513                 self.move_stack = self.move_stack[:-1]
514                 piece.x = x1
515                 piece.y = y1
516                 self.grid[x1][y1] = piece
517                 if target != None:
518                         target.x = x2
519                         target.y = y2
520                 self.grid[x2][y2] = target
521                 
522                 for p in self.pieces["white"] + self.pieces["black"]:
523                         p.possible_moves = None
524                 

UCC git Repository :: git.ucc.asn.au