summaryrefslogtreecommitdiff
path: root/examples/snake/main.lua
blob: 3a3243fd47ca2c2223452d597a47e0ad060ea9c0 (plain)
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
package.path = package.path..";"..RL.GetBasePath().."../resources/lib/?.lua"

Util = require( "utillib" )
Vec2 = require( "vector2" )
Rect = require( "rectangle" )

-- Defines
local RESOLUTION = Vec2:new( 128, 128 )
local TILE_SIZE = 8
local LEVEL_SIZE = RESOLUTION.x / TILE_SIZE
local STATE = { TITLE = 0, GAME = 1, OVER = 2 } -- Enum.

-- Resources
local framebuffer = nil
local monitor = 0
local monitorPos = Vec2:new( RL.GetMonitorPosition( monitor ) )
local monitorSize = Vec2:new( RL.GetMonitorSize( monitor ) )
local winScale = 6
local winSize = Vec2:new( RESOLUTION.x * winScale, RESOLUTION.y * winScale )
local gameState = STATE.GAME
local grassTexture = nil
local snakeTexture = nil
local appleTexture = nil
local gameSpeed = 7.0
local moveTimer = 1.0
local snake = {}
local applePos = {}

local function setSnake()
	snake = {
		heading = Vec2:new( 1, 0 ),
		control = Vec2:new( 1, 0 ),
		headPos = Vec2:new( LEVEL_SIZE / 2, LEVEL_SIZE / 2 ),
		segments = {}, -- { pos Vec2, heading Vec2 }
		grow = 2,
	}
end

local function addSegment()
	-- If first segment, grow from head and otherwise from tail. New segments are inserted firts.
	if #snake.segments == 0 then
		table.insert( snake.segments, 1, {
			pos = snake.headPos:clone(),
			heading = snake.heading:clone()
		} )
	else
		table.insert( snake.segments, 1, {
			pos = snake.segments[ #snake.segments ].pos:clone(),
			heading = snake.segments[ #snake.segments ].heading:clone()
		} )
	end
end

local function setApplePos()
	applePos = Vec2:new( math.random( 0, LEVEL_SIZE - 1 ), math.random( 0, LEVEL_SIZE - 1 ) )
	local search = true

	while search do
		search = false
		applePos = Vec2:new( math.random( 0, LEVEL_SIZE - 1 ), math.random( 0, LEVEL_SIZE - 1 ) )

		for _, seg in ipairs( snake.segments ) do
			search = applePos == seg.pos

			if search then
				break
			end
		end
	end
end

-- Init.

function RL.init()
	RL.SetWindowState( RL.FLAG_WINDOW_RESIZABLE )
	RL.SetWindowState( RL.FLAG_VSYNC_HINT )
	RL.SetWindowSize( winSize )
	RL.SetWindowPosition( { monitorPos.x + monitorSize.x / 2 - winSize.x / 2, monitorPos.y + monitorSize.y / 2 - winSize.y / 2 } )
	RL.SetWindowTitle( "Snake" )
	RL.SetWindowIcon( RL.LoadImage( RL.GetBasePath().."../resources/images/apple.png" ) )

	framebuffer = RL.LoadRenderTexture( RESOLUTION )
	grassTexture = RL.LoadTexture( RL.GetBasePath().."../resources/images/grass.png" )
	snakeTexture = RL.LoadTexture( RL.GetBasePath().."../resources/images/snake.png" )
	appleTexture = RL.LoadTexture( RL.GetBasePath().."../resources/images/apple.png" )

	setSnake()
	setApplePos()
end

-- Process.

local function moveSnake()
	-- Check if snake has eaten and should grow.
	if 0 < snake.grow then
		addSegment()
		snake.grow = snake.grow - 1
	end
	-- Move body.
	for i, seg in ipairs( snake.segments ) do
		if i < #snake.segments then
			seg.pos = snake.segments[ i+1 ].pos:clone()
			seg.heading = snake.segments[ i+1 ].heading:clone()
		else
			seg.pos = snake.headPos:clone()
			seg.heading = snake.heading:clone()
		end
	end
	-- Move head.
	snake.heading = Vec2:new( snake.control.x, snake.control.y )
	snake.headPos = Vec2:new( snake.headPos.x + snake.heading.x, snake.headPos.y + snake.heading.y )

	-- Check appple eating.
	if snake.headPos == applePos then
		snake.grow = snake.grow + 1
		setApplePos()
	end
	-- Check if hit to body.
	for _, seg in ipairs( snake.segments ) do
		if snake.headPos == seg.pos then
			gameState = STATE.OVER
		end
	end
	-- Check if outside or level.
	if snake.headPos.x < 0 or LEVEL_SIZE <= snake.headPos.x or snake.headPos.y < 0 or LEVEL_SIZE <= snake.headPos.y then
		gameState = STATE.OVER
	end

	moveTimer = moveTimer + 1.0
end

function RL.update( delta )
	if gameState == STATE.GAME then -- Run game.
		-- Controls.
		if RL.IsKeyPressed( RL.KEY_RIGHT ) and 0 <= snake.heading.x then
			snake.control = Vec2:new( 1, 0 )
		elseif RL.IsKeyPressed( RL.KEY_LEFT ) and snake.heading.x <= 0 then
			snake.control = Vec2:new( -1, 0 )
		elseif RL.IsKeyPressed( RL.KEY_DOWN ) and 0 <= snake.heading.y then
			snake.control = Vec2:new( 0, 1 )
		elseif RL.IsKeyPressed( RL.KEY_UP ) and snake.heading.y <= 0 then
			snake.control = Vec2:new( 0, -1 )
		end

		moveTimer = moveTimer - gameSpeed * delta

		if moveTimer <= 0.0 then
			moveSnake()
		end
	elseif gameState == STATE.OVER and RL.IsKeyPressed( RL.KEY_ENTER ) then -- Reset game.
		setSnake()
		setApplePos()
		gameState = STATE.GAME
	end
end

-- Drawing.

local function drawGrass()
	for y = 0, LEVEL_SIZE - 1 do
		for x = 0, LEVEL_SIZE - 1 do
			RL.DrawTexture( grassTexture, { x * TILE_SIZE, y * TILE_SIZE }, RL.WHITE )
		end
	end
end

--[[ Check if next segment is on left side. There are more mathematically elegant solution to this, but there is
only four possibilities so we can just check them all. ]]--
local function onLeft( this, nextSeg )
	return ( this == Vec2:new( 0, -1 ) and nextSeg == Vec2:new( -1, 0 ) )
		or ( this == Vec2:new( -1, 0 ) and nextSeg == Vec2:new( 0, 1 ) )
		or ( this == Vec2:new( 0, 1 ) and nextSeg == Vec2:new( 1, 0 ) )
		or ( this == Vec2:new( 1, 0 ) and nextSeg == Vec2:new( 0, -1 ) )
end

local function drawSnake()
	for i, seg in ipairs( snake.segments ) do
		local angle = seg.heading:atan2()
		local source = Rect:new( 16, 0, 8, 8 )

		if i == 1 then -- Tail segment. Yes tail is actually the 'first' segment.
			source.x = 8

			if 1 < #snake.segments then
				angle = snake.segments[ 2 ].heading:atan2()
			end
		elseif i < #snake.segments and seg.heading ~= snake.segments[ i+1 ].heading then -- Turned middle segments.
			source.x = 0
			-- Mirror turned segment to other way.
			if onLeft( seg.heading, snake.segments[ i+1 ].heading ) then
				source.height = -8
			end
		elseif i == #snake.segments and seg.heading ~= snake.heading then -- Turned segment before head.
			source.x = 0

			if onLeft( seg.heading, snake.heading ) then
				source.height = -8
			end
		end

		-- Notice that we set the origin to center { 4, 4 } that acts as pivot point. We also have to adjust our dest position by 4.
		RL.DrawTexturePro(
			snakeTexture,
			source,
			{ seg.pos.x * TILE_SIZE + 4, seg.pos.y * TILE_SIZE + 4, 8, 8 },
			{ 4, 4 },
			angle * RL.RAD2DEG,
			RL.WHITE
		)
	end
	-- Let's draw the head last to keep it on top.
	local angle = snake.heading:atan2()
	RL.DrawTexturePro(
		snakeTexture,
		{ 24, 0, 8, 8 },
		{ snake.headPos.x * TILE_SIZE + 4, snake.headPos.y * TILE_SIZE + 4, 8, 8 },
		{ 4, 4 },
		angle * RL.RAD2DEG,
		RL.WHITE
	)
end

local function drawApple()
	RL.DrawTexture( appleTexture, { applePos.x * TILE_SIZE, applePos.y * TILE_SIZE }, RL.WHITE )
end

function RL.draw()
	-- Clear the window to black.
	RL.ClearBackground( RL.BLACK )
	-- Draw to framebuffer.
	RL.BeginTextureMode( framebuffer )
		RL.ClearBackground( RL.BLACK )
		drawGrass()
		drawSnake()
		drawApple()

		if gameState == STATE.OVER then
			RL.DrawText( "Press Enter to\nrestart", { 10, 10 }, 10, RL.WHITE )
		end
	RL.EndTextureMode()

	-- Draw framebuffer to window.
	RL.DrawTexturePro(
		RL.GetRenderTextureTexture( framebuffer ),
		{ 0, 0, RESOLUTION.x, -RESOLUTION.y },
		{ 0, 0, winSize.x, winSize.y },
		{ 0, 0 },
		0.0,
		RL.WHITE
	)
end