In this activity, we will be re-making Noughts and Crosses with the Python programming language and using the Turtle Library.

Noughts and Crosses is a game where two players place an X or O in a 3 by 3 grid one turn at a time. You win by getting three of your marks in a line (horizontal, vertical, or diagonal).

We will write a program that will draw out the grid that the marks are drawn on and record the clicks of the players drawing a mark.

Taken and adapted from: Free Python Games

1– The grid

Go to trinkiet.io and start a new Python project: http://bit.ly/pythontrinket (log into your account if you have one or create one to save your progress). Then:

from turtle import *

Define a function line that takes parameters a, b, x, and y:

def line(a, b, x, y):
    up()
    goto(a, b)
    down()
    goto(x, y)

This will draw the lines of our grid

def grid():
    line(-67, 200, -67, -200)
    line(67, 200, 67, -200)
    line(-200, -67, 200, -67)
    line(-200, 67, 200, 67)

This draws the vertical and horizontal lines using the line function we just wrote.

You can check your frame is drawing properly by writing the following (after and outside the function definition) and clicking run:

Screen().setup(420, 420, 370, 0)
hideturtle()
Screen().tracer(0,0)
grid()
update()

2 – Drawing a mark

All the code in the following sections should go below the grid function and before the block starting Screen()… unless otherwise noted

Define a function drawx that takes the parameters x and y that will draw the cross mark for the X player:

def drawx(x, y):
    line(x, y, x + 133, y + 133)
    line(x, y + 133, x + 133, y)

Define a function drawo that takes the parameters x and y that will draw the nought mark for the O player:

def drawo(x, y):
    up()
    goto(x + 67, y + 5)
    down()
    circle(62)

3 – Remembering which player goes when

There are two players, nought and cross. Each draws a mark one after the other. We need to record whose turn it is. Create a dictionary, state and a list players:

state = {'player': 0}
players = [drawx, drawo]

4 – A little maths

We’re going to record where the user clicks the screen but we’ll need to get a location that matches up with the grid’s squares each time. Define a function floor that takes a parameter value.

def floor(value):
    return ((value + 200) // 133) * 133 - 200

5 – Tap it

We now have everything we need to write a function that organises drawing the correct type of mark in the right place. Define a function tap that takes the parameters x and y:

def tap(x, y):
    x = floor(x)
    y = floor(y)
    player = state['player']
    draw = players[player]
    draw(x, y)
    update()
    state['player'] = not player

This checks which player should be placing a counter (cross always starts), draws the correct mark and finally changes the player for the next turn.

6 – Final set up to play the game

We now need to call the tap function when the screen is clicked. Right at the end of all the code put the following (after the block beginning Screen()… finishing update()):

Screen().onscreenclick(tap)
done()

That’s it. Now you should be able to play Noughts and Crosses against a friend.

Solution on trinket

7– Challenges

Some challenge solutions

Add logic to prevent drawing several marks on the same square

main.py

from turtle import *
from state import *

def line(a, b, x, y):
    up()
    goto(a, b)
    down()
    goto(x, y)

def draw_grid():
    "Draw tic-tac-toe grid."
    line(-67, 200, -67, -200)
    line(67, 200, 67, -200)
    line(-200, -67, 200, -67)
    line(-200, 67, 200, 67)

def drawx(x, y):
    "Draw X player."
    line(x, y, x + 133, y + 133)
    line(x, y + 133, x + 133, y)

def drawo(x, y):
    "Draw O player."
    up()
    goto(x + 67, y + 5)
    down()
    circle(62)

def floor(value):
    "Round value down to grid with square size 133."
    return ((value + 200) // 133) * 133 - 200

state = State()

players = [drawx, drawo]


def tap(x, y):
    "Draw X or O in tapped square."
    x = floor(x)
    y = floor(y)
    player = state.player
    grid = state.grid;
    item = state.get_grid_item(x, y)
    
    if(grid[item[0]][item[1]] is not None):
      print "square already taken"
      return
    grid[item[0]][item[1]] = player
    draw = players[player]
    draw(x, y)
    update()
    state.player = not player
  
Screen().setup(420, 420, 370, 0)
hideturtle()
Screen().tracer(0, 0)
draw_grid()
update()
Screen().onscreenclick(tap)
done()

state.py

class State:
  def __init__(self):
    self.player = 0
    self.grid = [[None, None, None],
                 [None, None, None],
                 [None, None, None]]
  
  def get_grid_item(self, x, y):
    dic = {
      (-200,66):[0, 0],
      (-67,66):[0, 1],
      (66,66):[0, 2],
      (-200,-67):[1, 0],
      (-67,-67):[1, 1],
      (66,-67):[1, 2],
      (-200,-200):[2, 0],
      (-67,-200):[2, 1],
      (66,-200):[2, 2]
    }
    return dic[(x, y)]

Solution on trinket

How would you detect a winner (advanced!)

main.py

from turtle import *
from state import *
from check_win import *

def line(a, b, x, y):
    up()
    goto(a, b)
    down()
    goto(x, y)

def draw_grid():
    "Draw tic-tac-toe grid."
    line(-67, 200, -67, -200)
    line(67, 200, 67, -200)
    line(-200, -67, 200, -67)
    line(-200, 67, 200, 67)

def drawx(x, y):
    "Draw X player."
    line(x, y, x + 133, y + 133)
    line(x, y + 133, x + 133, y)

def drawo(x, y):
    "Draw O player."
    up()
    goto(x + 67, y + 5)
    down()
    circle(62)

def floor(value):
    "Round value down to grid with square size 133."
    return ((value + 200) // 133) * 133 - 200

state = State()

def tap(x, y):
    "Draw X or O in tapped square."
    Screen().onscreenclick(None)
    x = floor(x)
    y = floor(y)
    player = state.player
    grid = state.grid
    item = state.get_grid_item(x, y)
    
    if(grid[item[0]][item[1]] is not None):
      print "square already taken"
      return
    grid[item[0]][item[1]] = player
    if(player == state.players[0]):
      drawx(x, y)
    else:
      drawo(x, y)
    update()
    if(is_win(grid, item[0], item[1])):
      print("Player "+ str(player) + " wins")
      restart()
      return
    state.player = state.switch_player(player)
    Screen().onscreenclick(tap)

def restart():
  Screen().reset()
  hideturtle()
  state.reset()
  draw_grid()
  update()
  Screen().onscreenclick(tap)
  done()

  
Screen().setup(420, 420, 370, 0)
hideturtle()
Screen().tracer(0, 0)
draw_grid()
update()
Screen().onscreenclick(tap)
done()

check_win.py

def is_win(grid, row, column):
  if grid[0][column] == grid[1][column] == grid[2][column]:
    return True
  
  if grid[row][0] == grid[row][1] == grid[row][2]:
    return True
  
  if row == column and grid[0][0] == grid[1][1] == grid[2][2]:
    return True
  
  if row + column == 2 and grid[0][2] == grid[1][1] == grid[2][0]:
    return True
  
  return False 

state.py

class State:
  def __init__(self):
    self.players = ["X", "O"]
    self.player = self.players[0]
    self.grid = [[None, None, None],
                 [None, None, None],
                 [None, None, None]]
  
  def reset(self):
    self.player = self.switch_player(self.player)
    self.grid = [[None, None, None],
                 [None, None, None],
                 [None, None, None]]
                 
  def get_grid_item(self, x, y):
    dic = {
      (-200,66):[0, 0],
      (-67,66):[0, 1],
      (66,66):[0, 2],
      (-200,-67):[1, 0],
      (-67,-67):[1, 1],
      (66,-67):[1, 2],
      (-200,-200):[2, 0],
      (-67,-200):[2, 1],
      (66,-200):[2, 2]
    }
    return dic[(x, y)]
  
  def switch_player(self, player):
    if(player == self.players[0]):
      return self.players[1]
    else:
      return self.players[0]

Solution on trinket