forked from IanEThompson/OXrobot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNoughts_and_Crosses_with_Vision.py
591 lines (509 loc) · 22.1 KB
/
Noughts_and_Crosses_with_Vision.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
#
# Noughts and Crosses Game
# ========================
#
# Ian Thompson 2018
# 0414270210 ianmelandkids@gmail.com
#
# Plays noughts and crosses using a simple machine learning algorithm.
#
# The computer starts out completely "dumb"; it plays by the rules but has
# no clue how to win or lose. (except it can regonise a 'next move win' opportunity).
#
# But over time, it builds up two "experience" lists, which are essentially
# lists of "boards" (simplified so equivalent boards are combined), with votes
# saying whether that board is a good board (led to a win) or a bad board
# (led to a loss), and makes decisions using that experience.
#
# After a dozen or so games, it starts to play reasonably smart, and eventually
# becomes quite difficult, if not impossible to defeat.
#
# The board is represented by a string of exactly nine characters.
# Each character represents a particular position on the board:
#
# 0|1|2
# -----
# 3|4|5
# -----
# 6|7|8
#
# Each character can be "X" or "O" or " " (unoccupied)
# So the board:
#
# X|X|O
# -----
# X| |O
# -----
# O|X|
#
# Would be written as: "XXOX OOX "
import random #for choosing random moves
import collections #gamelist needs to be an ordered dictionary
import uArmFunctions #library of functions to control uArm
import computerVisionFunctions
import cv2 #opencv library
import numpy as np #opencv library support
import time #for time delays
"""%%%%%%%%%%%%% NORMAL ROBOT ARM AND GAME FUNCTIONS %%%%%%%%%%%%%%"""
def drawGrid():
uArmFunctions.goHome(uArm)
input("Clean board for new game. Press ENTER when ready")
print("Drawing Board")
uArmFunctions.drawBoard(uArm)
def drawLastMove(brd):
global lastDrawnBoard
global uArm
global computerGoesFirst
#draw any moves that haven't yet been drawn
for i in range(0,9):
if brd[i] != lastDrawnBoard[i]:
print("Drawing", brd[i], "in position", i)
if brd[i] == 'O':
if computerGoesFirst:
#0 will be drawn by the human
pass
else:
uArmFunctions.drawNought(uArm, i)
if brd[i] == 'X':
if computerGoesFirst:
uArmFunctions.drawCross(uArm, i)
else:
#X will be drawn by the human
pass
#update the record of what has already been drawn
lastDrawnBoard = brd
def printBrd(brd):
""" Outputs the board represented by 'brd' to the screen"""
print(brd[0],"|",brd[1],"|",brd[2], sep="")
print("-----")
print(brd[3],"|",brd[4],"|",brd[5], sep="")
print("-----")
print(brd[6],"|",brd[7],"|",brd[8], sep="")
def humanMoveVision(brd, video, board_lines):
""" Gets the human's next move using the screen and keyboard """
#determine which player is moving (X always moves first)
if brd.count("O") == brd.count("X"):
player="X"
else:
player="O"
newBrd=""
validMove=False
while not validMove: #keep looping until valid move
print("You are ", player, end=". ")
#play the game
#refresh the video stream
computerVisionFunctions.refreshWebcam(video)
_, original_image = video.read()
while True:
move_played, row, col = computerVisionFunctions.checkPlayerMove(video, board_lines, original_image)
if move_played:
break
else:
print("You did not make a move, please make a move")
#map rows and columns to 'move'
if row == 1 and col == 1:
move = 8
elif row == 1 and col == 2:
move = 7
elif row == 1 and col == 3:
move = 6
elif row == 2 and col == 1:
move = 5
elif row == 2 and col == 2:
move = 4
elif row == 2 and col == 3:
move = 3
elif row == 3 and col == 1:
move = 2
elif row == 3 and col == 2:
move = 1
elif row == 3 and col == 3:
move = 0
print("Your move was " + str(move) + ".")
try: #try converting to an int
#move=int(strMove[0])
if move<0 or move>8 or brd[move] != " ": #move in correct range and not a square already taken?
print("Invalid move - enter a move between 0 and 8:")
printBrd("012345678")
print("")
printBrd(brd)
else:
validMove=True
for i in range(0,9): #if valid int, add the move to the board
if i == move:
newBrd=newBrd + player
else:
newBrd = newBrd + brd[i]
except: #do this if the input wasn't an int
#if strMove=='x':
# printExperience()
#elif strMove=="load":
# loadExperience()
#else:
print("Invalid move - enter a move between 0 and 8:")
printBrd("012345678")
print("")
printBrd(brd)
return newBrd
def humanMove(brd):
""" Gets the human's next move using the screen and keyboard """
#determine which player is moving (X always moves first)
if brd.count("O") == brd.count("X"):
player="X"
else:
player="O"
newBrd=""
validMove=False
while not validMove: #keep looping until valid move
print("You are ", player, end=". ")
strMove = input("What is your move? (0-8): ")
try: #try converting to an int
move=int(strMove[0])
if move<0 or move>8 or brd[move] != " ": #move in correct range and not a square already taken?
print("Invalid move - enter a move between 0 and 8:")
printBrd("012345678")
print("")
printBrd(brd)
else:
validMove=True
for i in range(0,9): #if valid int, add the move to the board
if i == move:
newBrd=newBrd + player
else:
newBrd = newBrd + brd[i]
except: #do this if the input wasn't an int
if strMove=='x':
printExperience()
elif strMove=="load":
loadExperience()
else:
print("Invalid move - enter a move between 0 and 8:")
printBrd("012345678")
print("")
printBrd(brd)
return newBrd
def altIsGameWon(brd):
global winLine
for player in "OX":
if brd[0]==player and brd[1]==player and brd[2]==player:
winLine = "012"
return player
if brd[3]==player and brd[4]==player and brd[5]==player:
winLine = "345"
return player
if brd[6]==player and brd[7]==player and brd[8]==player:
winLine = "678"
return player
if brd[0]==player and brd[3]==player and brd[6]==player:
winLine = "036"
return player
if brd[1]==player and brd[4]==player and brd[7]==player:
winLine = "147"
return player
if brd[2]==player and brd[5]==player and brd[8]==player:
winLine = "258"
return player
if brd[0]==player and brd[4]==player and brd[8]==player:
winLine = "048"
return player
if brd[2]==player and brd[4]==player and brd[6]==player:
winLine = "246"
return player
#no winner, so check for a draw (board has no empty spaces)
if brd[:9].count(" ")==0:
return "D"
#If there's no winner, and no draw, the game is still underway:
return "N"
def isGameWon(brd):
""" Check to see if the game has been won:
Returns winner: 'X', 'O' or 'D' (Draw) or 'N' (no result yet)
Rotates a copy of the board 3 times looking for: three on the top row, three on the middle row, three diagonal
"""
#look for a winning combination by either X or O by checking for 3 patterns, with various rotations
for r in range(0,4):
for i in "OX":
if brd[0]==i and brd[1]==i and brd[2]==i:
return i
elif brd[0]==i and brd[4]==i and brd[8]==i:
return i
elif brd[3]==i and brd[4]==i and brd[5]==i:
return i
brd=tfRotate(brd)
#no winner, so check for a draw (board has no empty spaces)
if brd[:9].count(" ")==0:
return "D"
#If there's no winner, and no draw, the game is still underway:
return "N"
def nextMoves(brd):
""" Creates a list of all possible next moves
Assumes "X" always goes first when determining whose turn it is
"""
if brd.count("O") == brd.count("X"):
player="X"
else:
player="O"
nextMoveList=[]
for i in range(0,9):
if brd[i]==" ":
newBrd=""
for j in range(0,9):
if j == i:
newBrd=newBrd + player
else:
newBrd = newBrd + brd[j]
nextMoveList.append(newBrd)
return nextMoveList
def tfRotate(brd):
""" Returns a string representing the board rotated once clockwise """
newBrd=brd[6] + brd[3] + brd[0] + brd[7] + brd[4] + brd[1] + brd[8]+ brd[5] + brd[2]
return newBrd
def tfUnrotate(brd):
""" Returns a string representing the board rotated once anti-clockwise """
newBrd=brd[2] + brd[5] + brd[8] + brd[1] + brd[4] + brd[7] + brd[0]+ brd[3] + brd[6]
return newBrd
def tfFlip(brd):
""" Returns a string representing the board flipped (refelcted) about the diagonal"""
newBrd=brd[0] + brd[3] + brd[6] + brd[1] + brd[4] + brd[7] + brd[2]+ brd[5] + brd[8]
return newBrd
def tfToggle(brd):
""" Returns a string representing the board with O and X toggled"""
newBrd=""
for digit in brd:
if digit == 'X':
newBrd=newBrd + "O"
elif digit == 'O':
newBrd=newBrd + "X"
else:
newBrd=newBrd + " "
return newBrd
def tfInt(brd):
"""Returns the board as an int with
empty = 0
O = 1
X = 2
"""
newBrd=""
for digit in brd:
if digit == 'X':
newBrd=newBrd + "2"
elif digit == 'O':
newBrd=newBrd + "1"
else:
newBrd=newBrd + " "
return newBrd
def rootBoard(brd):
""" Returns a string representing a unique 'root' board for the given board.
This is because many different board positions are logically identical, just rotated or flipped
versions of a different board. This version matches those logically idential boards.
"""
rootScore=0 #the "score" of the highest scoring board
seqCount=0 #track the number of transforms so far
tfBrd=brd #the board after the latest transform
tfSequence="rrrrfrrrr"
#execute the next transform in the sequence
for tf in list(tfSequence):
if tf=="r":
tfBrd=tfRotate(tfBrd)
elif tf=="f":
tfBrd=tfFlip(tfBrd)
#score the transformed board by converting it into an integer
Score = int(tfBrd.replace("X","2").replace("O","1").replace(" ","0"))
#remember the best scoring board AND the number of transforms required to get to it
if Score > rootScore:
rootScore = Score
rootBrd = tfBrd
return rootBrd
def findBestMove(brd):
maxVotes=0
bestMove=random.choice(nextMoves(board))
if brd.count("O")<brd.count("X"): #if it's O's move
for m in nextMoves(brd):
if isGameWon(m)=="O": #if the move results in a win, just take it!
return m
if rootBoard(m) in O_Experience: #otherwise look for the best move in the experience list
if O_Experience[rootBoard(m)] > maxVotes:
bestMove=m
maxVotes = O_Experience[rootBoard(m)]
return bestMove
if brd.count("X")==brd.count("O"): #if it's X's move
for m in nextMoves(brd):
if isGameWon(m)=="X": #if the move results in a win, just take it!
return m
if rootBoard(m) in X_Experience: #otherwise look for the best move in the experience list
if X_Experience[rootBoard(m)] > maxVotes:
bestMove=m
maxVotes = X_Experience[rootBoard(m)]
return bestMove
def learnFromGame(Game):
""" Remembers the moves that lead to a win in the Xexperience or Oexperience dictionaries
"""
global X_Experience #use the global experience list for the X player
global O_Experience #use the global experience list for the O player
lastBrd = next(reversed(Game)) #get the last board in the Game to check the result
Winner = isGameWon(lastBrd)
if Winner == "X":
for g in Game:
if g.count("X")>g.count("O"): #every move of X's was good!
if g in X_Experience:
X_Experience[g]=X_Experience[g]+2 #if the move is known, increment vote by 2
else:
X_Experience[g]=2 #else add it into the experience with vote=2
else: #every move of O's was bad!
if g in O_Experience:
O_Experience[g]=O_Experience[g]-2 #if the move is known, decrement vote by 2
else:
O_Experience[g]=-2 #else add it into the experience with vote=-2
if Winner == "O":
for g in Game:
if g.count("O")==g.count("X"): #every move of O's was good!
if g in O_Experience:
O_Experience[g]=O_Experience[g]+2 #if the move is known, increment vote by 2
else:
O_Experience[g]=2 #else add it into the experience with vote=2
else: #every move of X's was bad!
if g in X_Experience:
X_Experience[g]=X_Experience[g]-2 #if the move is known, decrement vote
else:
X_Experience[g]=-2 #else add it into the experience with vote=-2
if Winner == "D": #If the game was a draw, that's better than 'unknown', so
for g in Game:
if g.count("X")>g.count("O"): #every move of X's was not great, but 'ok'!
if g not in X_Experience:
X_Experience[g]=1 #add it into the experience with vote=1
else: #every move of O's was also not great, but 'ok'!
if g in O_Experience:
O_Experience[g]=1 #else add it into the experience with vote=1
def printExperience():
#Print out X_Experience, showing votes
print("X Experience:")
for x in X_Experience:
print(x," : ",X_Experience[x])
#Print out O_Experience, showing votes
print("O Experience:")
for x in O_Experience:
print(x," : ",O_Experience[x])
print("")
def saveExperience():
'''Saves the experience dictionary into a file called 'experience.txt'''
print("Saving Experience:")
expFile = open("experience.txt", "w")
expFile.write("Game Count=" + str(gameCount) + "\n")
expFile.write("Experience for X:\n")
for x in X_Experience:
expFile.write(x + " : " + str(X_Experience[x]) + "\n")
expFile.write("Experience for O:\n")
for o in O_Experience:
expFile.write(o + " : " + str(O_Experience[o]) + "\n")
expFile.close()
def loadExperience():
'''Loads the experience from the file "experience.txt into the X_Experience and O_Experience'''
global X_Experience
global O_Experience
global gameCount
X_Experience={} #This is the list of boards after X's move with votes showing how good each board situation is
O_Experience={} #This is the list of boards after O's move with votes showing how good each board situation is
gameCount=0 #How many games have been played? (how experienced is the computer?)
loading=""
try:
with open("experience.txt", "r") as expFile:
fileLines=expFile.readlines()
for line in fileLines:
if line[:11] == "Game Count=":
gameCount=int(line[11:])
print("loading experience from", int(line[11:]), "games...")
elif line[:16] == "Experience for X":
loading="X"
elif line[:16] == "Experience for O":
loading="O"
elif line[10:11]==":":
brd,score = line.split(":")
if loading=="X":
X_Experience[brd[:9]]=int(score)
elif loading=="O":
O_Experience[brd[:9]]=int(score)
except IOError:
print("No experience file found")
# ========================
# MAIN PROGRAM STARTS HERE
# ========================
#Global Variables
X_Experience={} #This is the list of boards after X's move with votes showing how good each board situation is
O_Experience={} #This is the list of boards after O's move with votes showing how good each board situation is
gameCount=0 #How many games have been played? (how experienced is the computer?)
computerGoesFirst=False #who will go first next game?
computersTurn=False #keeps track of who's turn it is during a game
lastDrawnBoard=" " #This keeps track of which Os and Xs have already been drawn, so the program knows what to draw
winLine="000" #the line to draw after a won game
loadExperience() #if an experience file called 'experience.txt' exists in the program directory, load it!
uArm=uArmFunctions.openUArm('/dev/ttyACM0')
#begin video capture
video = cv2.VideoCapture(0)
#play games over and over
while True:
#Initialise the game
board=" "
GameList=collections.OrderedDict()
printBrd("012345678")
drawGrid() #get the robot arm to draw the grid
#identify the drawn board using computer vision
#reposition the camera to check the board
uArmFunctions.goVision(uArm)
#refresh the video stream
computerVisionFunctions.refreshWebcam(video)
#check the gameboard
_, frame = video.read()
time.sleep(2) #give the robot arm time to stabilize camera
cv2.imshow("Robot thinking", frame)
cv2.waitKey(50) #this time delay gives python time to show the image
is_gameboard, board_lines = computerVisionFunctions.detect_gameboard(frame)
if is_gameboard == False:
print("Gameboard could not be detected by camera. Trying gameboard detection again.")
_, frame = video.read()
time.sleep(2) #give the robot arm time to stabilize
cv2.imshow("Robot thinking", frame)
cv2.waitKey(50) #this time delay gives python time to show the image
is_gameboard, board_lines = computerVisionFunctions.detect_gameboard(frame)
if is_gameboard == False:
print("Gameboard could not be detected by camera. Exiting program.")
break
lastDrawnBoard=" " #the last drawn board was blank
computersTurn=computerGoesFirst #who's turn is it to go first?
if computersTurn:
print("\nStep aside human, I'm going first!")
else:
print("\nYou can go first")
#this is the main gaim loop. Break from the loop with the game if NOT "No result yet (N)" ie: when there is a result
while True:
if computersTurn:
board=findBestMove(board) #find the best move (based on experience)
printBrd(board) #display the move
computersTurn=False #computers turn is over
else:
#board=humanMove(board) #get the human's move
#reposition the camera to check the board
uArmFunctions.goVision(uArm)
time.sleep(2)
board = humanMoveVision(board, video, board_lines) #watch and process the human move
cv2.destroyAllWindows()
computersTurn=True #human's move is over
drawLastMove(board) #this finds the last move and gets the robot to draw it
GameList[rootBoard(board)]=0 #record the move (for analysis later)
if isGameWon(board)!="N": #check to see if the game is over
break
#when the game is over, declare the winner
print("")
gameResult = altIsGameWon(board)
if gameResult=="D":
print("The game was a draw")
else:
uArmFunctions.drawWinLine(uArm, winLine)
if computersTurn==True: #if the human won, the board still needs to be displayed
printBrd(board)
print(gameResult, "wins!")
print("")
learnFromGame(GameList) #remember all the moves from the game for next time!
saveExperience() #save experience to file
gameCount = gameCount + 1
print("Game Count = ", gameCount) #how many games have been played?
computerGoesFirst = not computerGoesFirst #take turns at going first
#printExperience() #display the experience lists (optional - uncomment if wanted)