· 5 years ago · Jul 16, 2020, 05:26 PM
1# this module is used to create the game user interface
2import pygame
3# this module is used to make HTTP requests to your machine learning model
4import requests
5# this module is used to choose a random colour for the user interface and
6# make random choices about moves the computer should make
7import random
8
9
10
11# API KEY - the unique private code for your Machine Learning project
12global KEY
13KEY = "put-your-project-API-key-here"
14
15
16
17
18############################################################################
19# Constants that match names you have used in your Machine Learning project
20############################################################################
21# You shouldn't need to change these, but if you spelled values differently
22# in your project, then you can update these to match here.
23
24# descriptions of the contents of a space on the game board
25EMPTY = "EMPTY"
26OPPONENT = "OPPONENT" # for the human, the OPPONENT is the computer
27 # for the computer, the OPPONENT is the human
28PLAYER = "PLAYER" # for the human, the PLAYER is the human
29 # for the computer, the PLAYER is the computer
30
31# descriptions of the locations on the game board
32top_left = "top_left"
33top_middle = "top_middle"
34top_right = "top_right"
35middle_left = "middle_left"
36middle_middle = "middle_middle"
37middle_right = "middle_right"
38bottom_left = "bottom_left"
39bottom_middle = "bottom_middle"
40bottom_right = "bottom_right"
41
42#
43############################################################################
44
45
46
47
48############################################################################
49# Converting between labels and numeric values
50############################################################################
51
52# training examples refer to a location on the game board
53deconvert = {}
54deconvert[top_left] = 0
55deconvert[top_middle] = 1
56deconvert[top_right] = 2
57deconvert[middle_left] = 3
58deconvert[middle_middle] = 4
59deconvert[middle_right] = 5
60deconvert[bottom_left] = 6
61deconvert[bottom_middle] = 7
62deconvert[bottom_right] = 8
63
64
65
66
67############################################################################
68# Machine Learning functions
69############################################################################
70
71# who the two players are
72HUMAN = "HUMAN"
73COMPUTER = "COMPUTER"
74
75# Storing a record of what has happened so the computer can learn from it!
76# contents of the board at each stage in the game
77gamehistory = {
78 HUMAN : [],
79 COMPUTER : []
80}
81# decisions made by each player
82decisions = {
83 HUMAN : [],
84 COMPUTER : []
85}
86
87
88
89# Use your machine learning model to decide where the
90# computer should move next.
91#
92# board : list of board spaces with the current state of each space
93# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
94def classify(board):
95 debug("Predicting the next best move for the computer")
96
97 # where should the request be sent?
98 url = "https://machinelearningforkids.co.uk/api/scratch/"+ KEY + "/classify"
99
100 # send the state of the game board to your machine learning model
101 response = requests.get(url, params={
102 "data" : get_board_from_perspective(board, COMPUTER)
103 })
104
105 if response.ok:
106 responseData = response.json()
107
108 # responseData will contain the list of predictions made by the
109 # machine learning model, starting from the one with the most
110 # confidence, to the one with the least confidence
111 for prediction in responseData:
112 # we can't make a move unless the space is empty, so
113 # check that first
114 if is_space_empty(board, prediction["class_name"]):
115 return prediction
116
117 # If we're here, it means that none of the predictions made by
118 # the machine learning model were actually empty!
119 # (This should never happen, but to be safe...)
120
121 # Pick a random space to move in
122 for space in random.sample(deconvert.keys(), len(deconvert)):
123 # we can't make a move unless the space is empty, so
124 # check that first
125 if is_space_empty(board, space):
126 return { "class_name" : space }
127 else:
128 # something went wrong - there was an error when trying to
129 # use your ML model
130 print(response.json())
131 response.raise_for_status()
132
133
134
135# Add a move that resulted in a win to the training data for the
136# machine learning model
137#
138# board : list of board spaces with the current state of each space
139# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
140# who : whose training data this is
141# e.g. HUMAN
142# name_of_space : name of the space that the move was in
143# e.g. bottom_left
144def add_to_train(board, who, name_of_space):
145 print ("Adding the move in %s by %s to the training data" % (name_of_space, who))
146
147 url = "https://machinelearningforkids.co.uk/api/scratch/"+ KEY + "/train"
148
149 response = requests.post(url, json={
150 # convert the contents of the board into a list of whose symbol
151 # is in that space, from the perspective of 'who'
152 # e.g. [ PLAYER, OPPONENT, PLAYER, EMPTY, EMPTY, PLAYER, OPPONENT, PLAYER, OPPONENT ]
153 "data" : get_board_from_perspective(board, who),
154 # the location that they chose to make a move in
155 "label" : name_of_space
156 })
157
158 if response.ok:
159 # training data stored okay
160 pass
161 else:
162 # something went wrong
163 print(response.json())
164 response.raise_for_status()
165
166
167
168# Train a new machine learning model using the training data
169# that has been collected so far
170def train_new_model():
171 print ("Training a new machine learning model")
172
173 url = "https://machinelearningforkids.co.uk/api/scratch/"+ KEY + "/models"
174 response = requests.post(url)
175
176 if response.ok:
177 # machine learning model trained successfully
178 pass
179 else:
180 # something went wrong
181 print(response.json())
182 response.raise_for_status()
183
184
185
186# Someone won the game.
187# A machine learning model could learn from this...
188#
189# winner : who won - either HUMAN or COMPUTER
190# boardhistory : the contents of the game board at each stage in the game
191# winnerdecisions : each of the decisions that the winner made
192def learn_from_this(winner, boardhistory, winnerdecisions):
193 print("%s won the game!" % (winner))
194 print("Maybe the computer could learn from %s's experience?" % (winner))
195 for idx in range(len(winnerdecisions)):
196 print("\nAt the start of move %d the board looked like this:" % (idx + 1))
197 print(boardhistory[idx])
198 print("And %s decided to put their mark in %s" % (winner, winnerdecisions[idx]))
199
200
201
202
203############################################################################
204# Noughts and Crosses logic
205############################################################################
206
207# get the location of a space on the board (an index from 0 to 8)
208#
209# This uses the lookup table 'deconvert' and copes with
210# student projects that have different spellings for
211# the spaces on the board.
212#
213# name_of_space : name of the space to check
214# e.g. middle_right
215def get_space_location(name_of_space):
216 # uses the default spelling if found
217 if name_of_space in deconvert:
218 return deconvert[name_of_space]
219 # otherwise tries the overrides
220 return deconvert[globals()[name_of_space]]
221
222
223# gets the contents of a space on the board
224#
225# board : list of board spaces with the contents of each space
226# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
227# name_of_space : name of the space to check
228# e.g. middle_right
229def get_space_contents(board, name_of_space):
230 return board[get_space_location(name_of_space)]
231
232
233# checks to see if a specific space on the board is currently empty
234#
235# board : list of board spaces with the contents of each space
236# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
237# name_of_space : name of the space to check
238# e.g. middle_right
239def is_space_empty(board, name_of_space):
240 return get_space_contents(board, name_of_space) == EMPTY
241
242
243
244# Creates the initial state for the game
245def create_empty_board():
246 debug("Creating the initial empty game board state")
247 return [ EMPTY, EMPTY, EMPTY,
248 EMPTY, EMPTY, EMPTY,
249 EMPTY, EMPTY, EMPTY ]
250
251
252
253# Gets the contents of the board, from the perspective of either
254# the human or the computer.
255#
256# board : list of board spaces with the current state of each space
257# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
258# who : either HUMAN or COMPUTER
259#
260# Returns the board described as PLAYER or OPPONENT
261# e.g. [ PLAYER, OPPONENT, PLAYER, EMPTY, EMPTY, PLAYER, OPPONENT, PLAYER, OPPONENT ]
262def get_board_from_perspective(board, who):
263 convertedboard = []
264 for move in board:
265 if move == EMPTY:
266 # an empty space is an empty space, from anyone's perspective
267 convertedboard.append(EMPTY)
268 else:
269 convertedboard.append(PLAYER if move == who else OPPONENT)
270 return convertedboard
271
272
273
274############################################################################
275# Noughts and Crosses user interface functions
276############################################################################
277
278# RGB colour codes
279WHITE = (255, 255, 255)
280RED = (255, 0, 0)
281GREEN = (0, 255, 0)
282
283
284game_board_coordinates = {}
285game_board_coordinates[top_left] = {
286 "bottom_left_corner": (120, 120),
287 "top_right_corner": (180, 180),
288 "top_left_corner": (180, 120),
289 "bottom_right_corner": (120, 180),
290 "centre": (150, 150)
291}
292game_board_coordinates[top_middle] = {
293 "bottom_left_corner": (220, 120),
294 "top_right_corner": (280, 180),
295 "top_left_corner": (220, 180),
296 "bottom_right_corner": (280, 120),
297 "centre": (250, 150)
298}
299game_board_coordinates[top_right] = {
300 "bottom_left_corner": (320, 120),
301 "top_right_corner": (380, 180),
302 "top_left_corner": (320, 180),
303 "bottom_right_corner": (380, 120),
304 "centre": (350, 150)
305}
306game_board_coordinates[middle_left] = {
307 "bottom_left_corner": (120, 220),
308 "top_right_corner": (180, 280),
309 "top_left_corner": (120, 280),
310 "bottom_right_corner": (180, 220),
311 "centre": (150, 250)
312}
313game_board_coordinates[middle_middle] = {
314 "bottom_left_corner": (220, 220),
315 "top_right_corner": (280, 280),
316 "top_left_corner": (220, 280),
317 "bottom_right_corner": (280, 220),
318 "centre": (250, 250)
319}
320game_board_coordinates[middle_right] = {
321 "bottom_left_corner": (320, 220),
322 "top_right_corner": (380, 280),
323 "top_left_corner": (320, 280),
324 "bottom_right_corner": (380, 220),
325 "centre": (350, 250)
326}
327game_board_coordinates[bottom_left] = {
328 "bottom_left_corner": (120, 320),
329 "top_right_corner": (180, 380),
330 "top_left_corner": (120, 380),
331 "bottom_right_corner": (180, 320),
332 "centre": (150, 350)
333}
334game_board_coordinates[bottom_middle] = {
335 "bottom_left_corner": (220, 320),
336 "top_right_corner": (280, 380),
337 "top_left_corner": (220, 380),
338 "bottom_right_corner": (280, 320),
339 "centre": (250, 350)
340}
341game_board_coordinates[bottom_right] = {
342 "bottom_left_corner": (320, 320),
343 "top_right_corner": (380, 380),
344 "top_left_corner": (320, 380),
345 "bottom_right_corner": (380, 320),
346 "centre": (350, 350)
347}
348
349
350# Check if someone has won and draws a line to show the winner
351# if someone has won
352#
353# who : Who made the last move? (only need to check if they won
354# as a player who hasn't just made a move can't have won)
355# e.g. HUMAN or COMPUTER
356#
357# Returns true if someone won
358# Returns false if noone won
359def display_winner(screen, board, who):
360 debug("Checking if %s has won" % (who))
361
362 gameover = False
363
364 # we use a green line if the human wins, a red line if the computer does
365 linecolour = GREEN if who == HUMAN else RED
366
367 ######## Rows ########
368 if get_space_contents(board, "top_left") == who and get_space_contents(board, "top_middle") == who and get_space_contents(board, "top_right") == who:
369 pygame.draw.line(screen, linecolour, (100, 150), (400, 150), 10)
370 gameover = True
371 if get_space_contents(board, "middle_left") == who and get_space_contents(board, "middle_middle") == who and get_space_contents(board, "middle_right") == who:
372 pygame.draw.line(screen, linecolour, (100, 250), (400, 250), 10)
373 gameover = True
374 if get_space_contents(board, "bottom_left") == who and get_space_contents(board, "bottom_middle") == who and get_space_contents(board, "bottom_right") == who:
375 pygame.draw.line(screen, linecolour, (100, 350), (400, 350), 10)
376 gameover = True
377
378 ######## Columns ########
379 if get_space_contents(board, "top_left") == who and get_space_contents(board, "middle_left") == who and get_space_contents(board, "bottom_left") == who:
380 pygame.draw.line(screen, linecolour, (150, 100), (150, 400), 10)
381 gameover = True
382 if get_space_contents(board, "top_middle") == who and get_space_contents(board, "middle_middle") == who and get_space_contents(board, "bottom_middle") == who:
383 pygame.draw.line(screen, linecolour, (250, 100), (250, 400), 10)
384 gameover = True
385 if get_space_contents(board, "top_right") == who and get_space_contents(board, "middle_right") == who and get_space_contents(board, "bottom_right") == who:
386 pygame.draw.line(screen, linecolour, (350, 100), (350, 400), 10)
387 gameover = True
388
389 ######## Diagonals #########
390 if get_space_contents(board, "top_left") == who and get_space_contents(board, "middle_middle") == who and get_space_contents(board, "bottom_right") == who:
391 pygame.draw.line(screen, linecolour, (100, 100), (400, 400), 15)
392 gameover = True
393 if get_space_contents(board, "bottom_left") == who and get_space_contents(board, "middle_middle") == who and get_space_contents(board, "top_right") == who:
394 pygame.draw.line(screen, linecolour, (400, 100), (100, 400), 15)
395 gameover = True
396
397 if gameover:
398 # refresh the display if we've drawn any game-over lines
399 pygame.display.update()
400
401 return gameover
402
403
404
405# Redraw the UI with a different background colour
406#
407# board : list of board spaces with the contents of each space
408# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
409def redraw_screen(screen, colour, board):
410 debug("Changing the background colour")
411
412 # fill everything in the new background colour
413 screen.fill(colour)
414
415 # now we've covered everything, we need to redraw
416 # the game board again
417 draw_game_board(screen)
418
419 # now we need to redraw all of the moves that
420 # have been made
421 for spacename in deconvert.keys():
422 space_code = deconvert[spacename]
423
424 if board[space_code] == HUMAN:
425 draw_move(screen, spacename, "cross")
426 elif board[space_code] == COMPUTER:
427 draw_move(screen, spacename, "nought")
428
429 # refresh now we've made changes
430 pygame.display.update()
431
432
433
434# Draw the crossed lines that make up a noughts and crosses board
435def draw_game_board(screen):
436 pygame.draw.rect(screen, WHITE, (195, 100, 10, 300))
437 pygame.draw.rect(screen, WHITE, (295, 100, 10, 300))
438 pygame.draw.rect(screen, WHITE, (100, 195, 300, 10))
439 pygame.draw.rect(screen, WHITE, (100, 295, 300, 10))
440
441
442
443# Setup the window that will be used to display the game
444def prepare_game_window():
445 debug("Setting up the game user interface")
446 # sets up the pygame library we'll use to create the game
447 pygame.init()
448 # create a window that is 500 pixels wide and 500 pixels high
449 screen = pygame.display.set_mode((500, 500))
450 # set the title of the window
451 pygame.display.set_caption("Machine Learning Noughts and Crosses")
452 return screen
453
454
455
456# Create a random RGB code to be used for the background colour
457def generate_random_colour():
458 debug("Generating a random colour code")
459 r = random.randint(0, 255)
460 g = random.randint(0, 255)
461 b = random.randint(0, 255)
462 return [r, g, b]
463
464
465
466# Draw a new move on the game board
467#
468# screen : The PyGame screen to draw the move on
469# name_of_space : Name of the space to draw the move on
470# e.g. middle_right
471# move : The move to draw.
472# It will be either "nought" or "cross"
473def draw_move(screen, name_of_space, move):
474 debug("Drawing a move on the game board : %s in %s" % (move, name_of_space))
475
476 if move == "nought":
477 location = game_board_coordinates[name_of_space]["centre"]
478 pygame.draw.circle(screen, WHITE, location, 35 , 8)
479 elif move == "cross":
480 pygame.draw.line(screen, WHITE,
481 game_board_coordinates[name_of_space]["bottom_left_corner"],
482 game_board_coordinates[name_of_space]["top_right_corner"],
483 10)
484 pygame.draw.line(screen, WHITE,
485 game_board_coordinates[name_of_space]["top_left_corner"],
486 game_board_coordinates[name_of_space]["bottom_right_corner"],
487 10)
488 pygame.display.update()
489
490
491
492# The user has clicked on the game board.
493# Which space did they click on?
494#
495# mx : the x coordinate of their click
496# my : the y coordiante of their click
497#
498# Returns the name of the space they clicked on (e.g. "middle_right")
499def get_click_location(mx, my):
500 debug("Getting location of click in %d,%d" % (mx, my))
501 if 100 < mx < 400 and 100 < my < 400:
502 if my < 200:
503 if mx < 200:
504 return top_left
505 elif mx < 300:
506 return top_middle
507 else:
508 return top_right
509 elif my < 300:
510 if mx < 200:
511 return middle_left
512 elif mx < 300:
513 return middle_middle
514 else:
515 return middle_right
516 else:
517 if mx < 200:
518 return bottom_left
519 elif mx < 300:
520 return bottom_middle
521 else:
522 return bottom_right
523 return "none"
524
525
526
527# Handle a new move, by either the player or the computer
528#
529# board : list of board spaces with the contents of each space
530# e.g. [ HUMAN, COMPUTER, HUMAN, EMPTY, EMPTY, HUMAN, COMPUTER, HUMAN, COMPUTER ]
531# name_of_space : name of the space the move was in
532# e.g. middle_right
533# identity : whose move this is
534# e.g. HUMAN or COMPUTER
535#
536# returns true if this move ended the game
537# returns false if the game should keep going
538def game_move(screen, board, name_of_space, identity):
539 debug("Processing a move for %s who chose %s" % (identity, name_of_space))
540
541 # choose the symbol for which player this is
542 symbol = "cross" if identity == HUMAN else "nought"
543
544 # draw a symbol on the board to represent the move
545 draw_move(screen, name_of_space, symbol)
546
547 # update the history of what has happened in case
548 # we want to learn from it later
549 gamehistory[identity].append(board.copy())
550 decisions[identity].append(name_of_space)
551
552 # update the board to include the move
553 movelocation = get_space_location(name_of_space)
554 board[movelocation] = identity
555
556 # have they won the game?
557 gameover = display_winner(screen, board, identity)
558 if gameover:
559 # someone won! maybe an ML project could learn from this
560 learn_from_this(identity, gamehistory[identity], decisions[identity])
561
562 # the game is also over if the board is full (a draw!)
563 #
564 # and the board is full if both players together
565 # have made 9 moves in total
566 if len(decisions[HUMAN]) + len(decisions[COMPUTER]) >= 9:
567 gameover = True
568
569 return gameover
570
571
572
573# the machine learning model's turn
574def let_computer_play(screen, board):
575 computer_move = classify(board)
576 print(computer_move)
577 return game_move(screen, board, computer_move["class_name"], COMPUTER)
578
579
580
581
582############################################################################
583# Main game logic starts here
584############################################################################
585
586def debug(msg):
587 # if something isn't working, uncomment the line below
588 # so you get detailed print-outs of everything that
589 # the program is doing
590 # print(msg)
591 pass
592
593
594
595debug("Configuration")
596debug("Using identities %s %s %s" % (EMPTY, PLAYER, OPPONENT))
597debug(deconvert)
598
599debug("Initial startup and setup")
600screen = prepare_game_window()
601board = create_empty_board()
602redraw_screen(screen, generate_random_colour(), board)
603
604debug("Initialising game state variables")
605running = True
606gameover = False
607
608debug("Deciding who will play first")
609computer_goes_first = random.choice([False, True])
610if computer_goes_first:
611 let_computer_play(screen, board)
612
613
614while running:
615 # wait for the user to do something...
616 event = pygame.event.wait()
617
618 if event.type == pygame.QUIT:
619 running = False
620
621 if event.type == pygame.MOUSEBUTTONDOWN and gameover == False:
622 # what has the user clicked on?
623 mx, my = pygame.mouse.get_pos()
624 location_name = get_click_location(mx, my)
625
626 if location_name == "none":
627 # user clicked on none of the spaces so we'll
628 # change the colour for them instead!
629 redraw_screen(screen, generate_random_colour(), board)
630
631 elif is_space_empty(board, location_name):
632 # the user clicked on an empty space
633 gameover = game_move(screen, board, location_name, HUMAN)
634
635 # if we're still going, it is the computer's turn next
636 if gameover == False:
637 # the computer chooses where to play
638 gameover = let_computer_play(screen, board)
639
640 # ignore anything else the user clicked on while we
641 # were processing their click, so they don't try to
642 # sneakily have lots of moves at once
643 pygame.event.clear()