Basics: Tic-tac-toe

This tutorial will teach you about some basic features (drawing primitives, colors, surface, fonts, mouse events) of Drystal by creating a Tic-tac-toe game.

../_images/basics_final.gif

Getting our first window

First of all we need to get the Drystal table:

local drystal = require 'drystal'

Then, we define drystal.init() which will be called by Drystal when the script will be loaded.

In this function we call drystal.resize() with the width and height of our window.

1
2
3
4
5
6
local drystal = require 'drystal'
local W, H = 400, 400

function drystal.init()
    drystal.resize(W, H)
end

Now that we have a window we define drystal.draw() which will be called by Drystal when drawing the window. This function will contain all the code needed to have beautiful graphics for our game. We start by drawing a black background and through this tutorial you will see how we can complete this function.

When using Drystal drawing functions you need to set the color you want to use. For this there is drystal.set_color(), it takes RGB values (from 0 to 255) or a Color.

Note

Drystal provides a table, drystal.colors, which contains all of the W3C colors.

After setting the right color, you just call drystal.draw_background() and it’s done.

function drystal.draw()
    drystal.set_color(drystal.colors.black)
    drystal.draw_background()
end
../_images/basics_black.png

Congratulation, you managed to get a fully functional window with a beautiful black background with Drystal! Don’t worry this is just the beginning of a wonderful adventure!

Drawing the board

Having a black background is nice but we need to show our players where to put X or O. So let’s draw a simple board with white lines, shall we?

For this we reuse drystal.set_color() and use drystal.draw_line() which draws a line between two points. And we reuse the W and H variables (width and height) to draw lines that fit the window.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
local function draw_board()
    drystal.set_color(drystal.colors.white)
    -- horizontal lines
    drystal.draw_line(0, H * 1 / 3, W, H * 1 / 3)
    drystal.draw_line(0, H * 2 / 3, W, H * 2 / 3)
    -- vertical lines
    drystal.draw_line(W * 1 / 3, 0, W * 1 / 3, H)
    drystal.draw_line(W * 2 / 3, 0, W * 2 / 3, H)
end

function drystal.draw()
    drystal.set_alpha(255)
    drystal.set_color(drystal.colors.black)
    drystal.draw_background()

    draw_board()
end
../_images/basics_board.png

Drawing sprites

Now we focus on drawing the noughts and crosses on the board by using sprites.

Loading the resources

First, we use our favorite image manipulation program to get a spritesheet with our graphics. Then, we load this spritesheet with drystal.load_surface() which give us a Surface that we can manipulate.

Finally, we define a table containing the location of the sprites and their size. It will be needed later when we will draw the sprites.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local drystal = require 'drystal'

local W, H = 400, 400

-- load resources
local spritesheet = drystal.load_surface('spritesheet.png')
local sprites = {
    { x=0, y=0, w=128, h=128 }, -- O
    { x=128, y=0, w=128, h=128 }, -- X
}

Let’s draw!

Now that we have our resources we can tell Drystal what to draw at each frame.

First, we keep a state of the game (i.e. where are the marks) and we define a matrix of 9 by 9 where an empty string represents a free tile, x the player one, o the player two.

1
2
3
4
5
local board = {
    {'o', '', ''},
    {'o', '', 'x'},
    {'x', '', ''},
}

Second, we tell Drystal from which Surface to draw from, we can use the method Surface:draw_from() for this:

local function draw_marks()
    spritesheet:draw_from()

Last, for each tile in the matrix we draw a sprite or nothing according to its content.

Different functions can be used to draw sprites from the current surface. For this game we use drystal.draw_sprite_resized() which allows us to resize a sprite before drawing it. Since, we divided the window into 9 tiles, the size of one tile is W / 3 and H / 3.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local function draw_marks()
    spritesheet:draw_from()
    for i = 1, 3 do
        for j = 1, 3 do
            local mark = board[i][j]
            local x = W * (j - 1) / 3
            local y = H * (i - 1) / 3
            if mark == 'x' then
                drystal.draw_sprite_resized(sprites[2], x, y, W / 3, H / 3)
            elseif mark == 'o' then
                drystal.draw_sprite_resized(sprites[1], x, y, W / 3, H / 3)
            end
        end
    end
end

function drystal.draw()
    drystal.set_color(drystal.colors.black)
    drystal.draw_background()

    draw_board()
    draw_marks()
end
../_images/basics_sprites.png

Getting mouse events

We have a game that currently only display a hardcoded state of a game and I think we can agree on the fact that this is not really a funny game. We need to let the players interact by clicking on the tile where they want to make a move.

Drystal calls drystal.mouse_press() whenever a mouse button is pressed with the coordinate and which button is pressed. We use this function to call another function that plays a turn according to the current player.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local function play()
    local i, j = highlighted_tile.i, highlighted_tile.j

    -- is there already a nought or a cross on this tile ?
    if board[i][j] ~= '' then
        return
    end

    -- set the tile
    board[i][j] = current_player

    -- switch player
    if current_player == 'x' then
        current_player = 'o'
    else
        current_player = 'x'
    end
end

function drystal.mouse_press(x, y, button)
    if button == drystal.buttons.left then
        play()
    end
end

Is there a winner ?

Unfortunately, the game can not stop currently. For each move we need to check if one of the players won the game, this way we can display a gameover screen and restart the game.

Checking if someone won in a tic-tac-toe game is fairly simple, we check if there is the same mark in a horizontal, vertical or diagonal line:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
local function check_winner()
    -- horizontal lines
    for i = 1, 3 do
        if board[i][1] ~= '' and board[i][1] == board[i][2] and board[i][2] == board[i][3] then
            return true
        end
    end
    -- vertical lines
    for j = 1, 3 do
        if board[1][j] ~= '' and board[1][j] == board[2][j] and board[2][j] == board[3][j] then
            return true
        end
    end
    -- diagonal lines
    if board[1][1] ~= '' and board[1][1] == board[2][2] and board[2][2] == board[3][3] then
        return true
    end
    if board[3][1] ~= '' and board[3][1] == board[2][2] and board[2][2] == board[1][3] then
        return true
    end
    return false
end

There is also the case where no one won, so we check if the board is full:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local function check_board_full()
    for i = 1, 3 do
        for j = 1, 3 do
            if board[i][j] == '' then
                return false
            end
        end
    end
    return true
end

This ways, each time a player makes a move, we call check_winner() and check_board_full() to know if the game ended:

 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
local function play()
    local i, j = highlighted_tile.i, highlighted_tile.j

    -- is there already a nought or a cross on this tile ?
    if board[i][j] ~= '' then
        return
    end

    -- set the tile
    board[i][j] = current_player

    if check_winner() then
        ended = true
    elseif check_board_full() then
        ended = true
        current_player = ''
    else
        -- switch player
        if current_player == 'x' then
            current_player = 'o'
        else
            current_player = 'x'
        end
    end
end

We also make a function to restart the game when it is finished and a player clicked on the window:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
local function restart()
    ended = false
    current_player = 'x'
    board = {
        {'', '', ''},
        {'', '', ''},
        {'', '', ''},
    }
end

function drystal.mouse_press(x, y, button)
    if button == drystal.buttons.left then
        if ended then
            restart()
        else
            play()
        end
    end
end

Gameover screen

The players would like to know who won the game or if there is a tie, so we need a proper gameover screen.

First, we load two fonts with drystal.load_font() that will be used to write on the window who won:

-- load resources
local font = assert(drystal.load_font('arial.ttf', 40))
local smallfont = assert(drystal.load_font('arial.ttf', 24))

Then, we draw a transparent rectangle which fade the window by using drystal.set_alpha() and drystal.draw_rect(). The former modifying the alpha channel which control the transparency and the latter drawing a rectangle that we use to draw a rectangle the size of the window.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
local function draw_gameover()
    -- fade the screen
    drystal.set_alpha(150)
    drystal.set_color(drystal.colors.black)
    drystal.draw_rect(0, 0, W, H)

    local text
    if current_player == 'x' then
        text = "Player 1 won the game!"
    elseif current_player == 'o' then
        text = "Player 2 won the game!"
    else
        text = "It's a tie!"
    end
    local _, htext = font:sizeof(text)
    drystal.set_alpha(255)
    drystal.set_color(drystal.colors.white)
    font:draw(text, W / 2, H / 2 - htext / 2, drystal.aligns.center)

    smallfont:draw('Click to restart', W / 2, H * .7, drystal.aligns.center)
end

Finally, we update drystal.draw() so that we call draw_gameover() when ended is true:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function drystal.draw()
    drystal.set_color(drystal.colors.black)
    drystal.draw_background()

    draw_board()
    draw_marks()

    -- draw the winner
    if ended then
        draw_gameover()
    end
end
../_images/basics_gameover.png

Bonus: Highlighting the tiles

We want the current player to know where the pointer is. For this we define a table highlighted_tile and we update it with our definition of drystal.mouse_motion() which will be called when the mouse is moved by the player.

1
2
3
4
5
6
7
8
local highlighted_tile = {i=-1, j=-1}

function drystal.mouse_motion(x, y)
    highlighted_tile = {
        i = math.ceil(y / W * 3),
        j = math.ceil(x / H * 3),
    }
end

Since we have the information about the mouse position we can draw the sprite (nought or cross) but with a bit of transparency to show the difference between the pointer and a real cross or nought.

 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
local function draw_marks()
    spritesheet:draw_from()
    for i = 1, 3 do
        for j = 1, 3 do
            local mark = board[i][j]
            local x = W * (j - 1) / 3
            local y = H * (i - 1) / 3
            if mark == 'x' then
                drystal.set_alpha(255)
                drystal.draw_sprite_resized(sprites[2], x, y, W / 3, H / 3)
            elseif mark == 'o' then
                drystal.set_alpha(255)
                drystal.draw_sprite_resized(sprites[1], x, y, W / 3, H / 3)
            elseif i == highlighted_tile.i and j == highlighted_tile.j then
                drystal.set_alpha(100)
                local sprite = current_player == 'x' and 2 or 1
                drystal.draw_sprite_resized(sprites[sprite], x, y, W / 3, H / 3)
            end
        end
    end
end

function drystal.draw()
    drystal.set_alpha(255)
    drystal.set_color(drystal.colors.black)
    drystal.draw_background()

    draw_board()
    draw_marks()

    -- draw the winner
    if ended then
        draw_gameover()
    end
end

Final code

Here is the complete code of this tutorial, don’t hesitate to improve it with your ideas, and if you want to know more go to the next tutorial where we will show you how to do a space invader!

  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
local drystal = require 'drystal'

local W, H = 400, 400

-- load resources
local font = assert(drystal.load_font('arial.ttf', 40))
local smallfont = assert(drystal.load_font('arial.ttf', 24))
local spritesheet = drystal.load_surface('spritesheet.png')
local sprites = {
	{ x=0, y=0, w=128, h=128 }, -- O
	{ x=128, y=0, w=128, h=128 }, -- X
}

-- game info
local ended = false
local current_player = 'x'
local board = {
	{'', '', ''},
	{'', '', ''},
	{'', '', ''},
}
local highlighted_tile = {i=-1, j=-1}

function drystal.init()
	drystal.resize(W, H)
end

local function draw_board()
	drystal.set_color(drystal.colors.white)
	-- horizontal lines
	drystal.draw_line(0, H * 1 / 3, W, H * 1 / 3)
	drystal.draw_line(0, H * 2 / 3, W, H * 2 / 3)
	-- vertical lines
	drystal.draw_line(W * 1 / 3, 0, W * 1 / 3, H)
	drystal.draw_line(W * 2 / 3, 0, W * 2 / 3, H)
end

local function draw_marks()
	spritesheet:draw_from()
	for i = 1, 3 do
		for j = 1, 3 do
			local mark = board[i][j]
			local x = W * (j - 1) / 3
			local y = H * (i - 1) / 3
			if mark == 'x' then
				drystal.set_alpha(255)
				drystal.draw_sprite_resized(sprites[2], x, y, W / 3, H / 3)
			elseif mark == 'o' then
				drystal.set_alpha(255)
				drystal.draw_sprite_resized(sprites[1], x, y, W / 3, H / 3)
			elseif i == highlighted_tile.i and j == highlighted_tile.j then
				drystal.set_alpha(100)
				local sprite = current_player == 'x' and 2 or 1
				drystal.draw_sprite_resized(sprites[sprite], x, y, W / 3, H / 3)
			end
		end
	end
end

local function draw_gameover()
	-- fade the screen
	drystal.set_alpha(150)
	drystal.set_color(drystal.colors.black)
	drystal.draw_rect(0, 0, W, H)

	local text
	if current_player == 'x' then
		text = "Player 1 won the game!"
	elseif current_player == 'o' then
		text = "Player 2 won the game!"
	else
		text = "It's a tie!"
	end
	local _, htext = font:sizeof(text)
	drystal.set_alpha(255)
	drystal.set_color(drystal.colors.white)
	font:draw(text, W / 2, H / 2 - htext / 2, drystal.aligns.center)

	smallfont:draw('Click to restart', W / 2, H * .7, drystal.aligns.center)
end

function drystal.draw()
	drystal.set_alpha(255)
	drystal.set_color(drystal.colors.black)
	drystal.draw_background()

	draw_board()
	draw_marks()

	-- draw the winner
	if ended then
		draw_gameover()
	end
end

local function check_winner()
	-- horizontal lines
	for i = 1, 3 do
		if board[i][1] ~= '' and board[i][1] == board[i][2] and board[i][2] == board[i][3] then
			return true
		end
	end
	-- vertical lines
	for j = 1, 3 do
		if board[1][j] ~= '' and board[1][j] == board[2][j] and board[2][j] == board[3][j] then
			return true
		end
	end
	-- diagonal lines
	if board[1][1] ~= '' and board[1][1] == board[2][2] and board[2][2] == board[3][3] then
		return true
	end
	if board[3][1] ~= '' and board[3][1] == board[2][2] and board[2][2] == board[1][3] then
		return true
	end
	return false
end

local function check_board_full()
	for i = 1, 3 do
		for j = 1, 3 do
			if board[i][j] == '' then
				return false
			end
		end
	end
	return true
end

local function play()
	local i, j = highlighted_tile.i, highlighted_tile.j

	-- is there already a nought or a cross on this tile ?
	if board[i][j] ~= '' then
		return
	end

	-- set the tile
	board[i][j] = current_player

	if check_winner() then
		ended = true
	elseif check_board_full() then
		ended = true
		current_player = ''
	else
		-- switch player
		if current_player == 'x' then
			current_player = 'o'
		else
			current_player = 'x'
		end
	end
end

local function restart()
	ended = false
	current_player = 'x'
	board = {
		{'', '', ''},
		{'', '', ''},
		{'', '', ''},
	}
end

function drystal.mouse_motion(x, y)
	highlighted_tile = {
		i = math.ceil(y / W * 3),
		j = math.ceil(x / H * 3),
	}
end

function drystal.mouse_press(x, y, button)
	if button == drystal.buttons.left then
		if ended then
			restart()
		else
			play()
		end
	end
end