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.
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
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
|
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
|
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
|
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
|