View difference between Paste ID: hFubxDgx and EVXXNadV
SHOW: | | - or go back to the newest paste.
1
SCORE_Y = 1			--Y value to draw the score text; it'll be centered on the X axis
2
CONTROLS_Y = 18		--Y value to draw RESET button; it'll be centered on the X axis
3
ANIMATION_TIME = 0.1--Animation sleep time, lower is faster
4
5
m = nil --Monitor peripheral, if connected
6
7
function getMonitor()
8
	--Sets m to equal an attached monitor if available and large enough.
9
	--Returns true if a monitor is found
10
	local i = 1
11
	local sides = rs.getSides() --Returns a list of sides (e.g. "left", "back", etc)
12
	while i <= #sides and m == nil do --Loop every side or until a monitor is found
13
		if peripheral.getType(sides[i]) == "monitor" then
14
			local w, h = peripheral.call(sides[i], "getSize")
15
			if w >= 18 and h >= 19 then --Attached monitor must be at least 18x19
16
				print("Monitor detected.  Outputting game to "..sides[i].." monitor.")
17
				m = peripheral.wrap(sides[i])
18
				return true
19
			else
20
				print("Monitor size "..w.."x"..h.." detected but at least 18x19 is required.")
21
				print("Press any key to continue...")
22-
				os.pullEvent("key") --Wait for a keypress
22+
				sapphUtils.pullEvent("key") --Wait for a keypress
23
				term.clear()
24
			end
25
		end
26
		i = i + 1
27
	end
28
	
29
	return false
30
end
31
32
function drawScreen()
33
	--Draws all screen elements.  Use drawScore() and drawGrid() to refresh just those
34
	m.setBackgroundColor(colors.black)
35
	m.clear()
36
	
37
	--Draw the border
38
	m.setBackgroundColor(colors.white)
39
	for x = tlGridX - 1, tlGridX + 16 do --Top & bottom borders
40
		m.setCursorPos(x, tlGridY - 1)
41
		m.write(" ")
42
		m.setCursorPos(x, tlGridY - 1 + 13)
43
		m.write(" ")
44
	end
45
	for y = tlGridY - 1, tlGridY + 12 do --Left & right borders
46
		m.setCursorPos(tlGridX - 1, y)
47
		m.write(" ")
48
		m.setCursorPos(tlGridX - 1 +  17, y)
49
		m.write(" ")
50
	end
51
	
52
	if isValidMonitorConnected then
53
		--Draw reset button
54
		m.setBackgroundColor(colors.orange)
55
		m.setTextColor(colors.white)
56
		m.setCursorPos(termWidth / 2 - string.len("RESET") / 2 + 1, CONTROLS_Y)
57
		m.write("RESET")
58
		
59
		--Draw quit "X" at top right
60
		m.setBackgroundColor(colors.red)
61
		m.setCursorPos(termWidth, 1)
62
		m.write("X")
63
	else
64
		--Draw control description
65
		m.setBackgroundColor(colors.black)
66
		m.setTextColor(colors.white)
67
		m.setCursorPos(1, 8)
68
		m.write("Controls:")
69
		m.setCursorPos(1, 9)
70
		m.write("Arrow keys")
71
		m.setCursorPos(1, 10)
72
		m.write("(R)eset")
73
		m.setCursorPos(1, 11)
74-
		m.write("E(x)it")
74+
		m.write("Switch site - X")
75
	end
76
	
77
	drawScore()
78
79
	drawGrid()
80
end
81
82
function drawScore()
83
	m.setBackgroundColor(colors.black)
84
	m.setTextColor(colors.white)
85
	scoreText = "Score: "..score
86
	m.setCursorPos(termWidth / 2 - string.len(scoreText) / 2 + 1, SCORE_Y) --Center the score on the X axis
87
	m.write(scoreText)	
88
end
89
90
function drawGrid()
91
	for r = 0, 3 do --Row
92
		for c = 0, 3 do --Column
93
			drawSquare(r, c, grid[r][c])
94
		end
95
	end
96
end
97
 
98
function drawSquare(r, c, value, randomColor)
99
	--Draws an individual square on the board
100
	--Board squares are 4 wide and 3 tall so multiply c & r values respectively
101
	--randomColor and -1 value used for intro sequence only
102
	if value == 0 then --Black out the square
103
		m.setBackgroundColor(colors.black)
104
		m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
105
		m.write("    ")
106
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
107
		m.write("    ")
108
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
109
		m.write("    ")
110
	elseif value == -1 then --Only used for rendering the 0, which normally is invisible, during the intro sequence
111
		if not randomColor then
112
			m.setBackgroundColor(colors.green)
113
		else
114
			m.setBackgroundColor(2^math.random(2, 14)) --Any color but white or black
115
		end
116
		m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
117
		m.write("    ")
118
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
119
		m.write("  0 ")
120
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
121
		m.write("    ")		
122
	else
123
		if not randomColor then
124
			m.setBackgroundColor(value)
125
		else
126
			m.setBackgroundColor(2^math.random(2, 14)) --Any color but white or black
127
		end
128
		m.setCursorPos(c * 4 + tlGridX, r * 3 + tlGridY)
129
		m.write("    ")
130
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 1 + tlGridY)
131
		--Center the number string within spaces (e.g. "  2 " or " 32 ")
132
		local strlen = string.len(value) 
133
		if strlen == 1 then
134
			m.write("  "..value.." ")
135
		elseif strlen == 2 then
136
			m.write(" "..value.." ")
137
		elseif strlen == 3 then
138
			m.write(" "..value)
139
		else
140
			m.write(tostring(value))
141
		end
142
		m.setCursorPos(c * 4 + tlGridX, r * 3 + 2 + tlGridY)
143
		m.write("    ")				
144
	end	
145
end
146
147
function clearGrid()
148
	--Initializes the grid
149
	for r = 0, 3 do
150
		grid[r] = {}
151
		for c = 0, 3 do
152
			grid[r][c] = 0
153
		end
154
	end	
155
end
156
 
157
function placeNewTile()
158
	--Places a 2 or 4 at a random empty location in the grid
159
	local number
160
	
161
	if math.random(10) == 10 then --Place a 4 10% of the time, else 2
162
		number = 4
163
	else
164
		number = 2
165
	end
166
	--Get a count of empty cells in the grid
167
	local emptyCount = 0
168
	for r = 0, 3 do
169
		for c = 0, 3 do
170
			if grid[r][c] == 0 then
171
				emptyCount = emptyCount + 1
172
			end
173
		end
174
	end
175
	--Choose an empty cell and place the number
176
	local target = math.random(emptyCount)
177
	emptyCount = 0
178
	for r = 0, 3 do
179
		for c = 0, 3 do
180
			if grid[r][c] == 0 then
181
				emptyCount = emptyCount + 1
182
				if emptyCount == target then
183
					grid[r][c] = number
184
				end
185
			end
186
		end
187
	end
188
end
189
 
190
function transpose()
191
	--[[ Turns rows into columns and vice-versa, like so:
192
		0, 1, 2, 3, 			0, 4, 8, C,
193
		4, 5, 6, 7,				1, 5, 9, D,
194
		8, 9, A, B,		becomes	2, 6, A, E,
195
		C, D, E, F				3, 7, B, F
196
	]]--
197
    local res = {}
198
 
199
    for r = 0, 3 do
200
        res[r] = {}
201
        for c = 0, 3 do
202
            res[r][c] = grid[c][r]
203
        end
204
    end
205
 
206
    grid = res
207
end
208
209
function rotateClockwise()
210
	transpose() --Transposing then reversing the order of the rows is equivalent to rotating the grid clockwise
211
	reverseRows()
212
end
213
 
214
function rotateCounterClockwise()
215
	transpose() --Transposing then reversing the order of the columns is equivalent to rotating the grid counterclockwise
216
	reverseColumns()
217
end
218
 
219
function reverseRows()
220
	for r = 0, 3 do
221
		grid[r][0], grid[r][3] = grid[r][3], grid[r][0] --Swap the first and fourth values
222
		grid[r][1], grid[r][2] = grid[r][2], grid[r][1] --Swap the second and third values
223
	end 
224
end
225
 
226
function reverseColumns()
227
	for c = 0, 3 do
228
		grid[0][c], grid[3][c] = grid[3][c], grid[0][c] --Swap the first and fourth values
229
		grid[1][c], grid[2][c] = grid[2][c], grid[1][c] --Swap the second and third values
230
	end 
231
end
232
233
function collapseLeft()
234
	--For each empty (0) cell, shift the next non-zero number into the cell
235
	--For each cell with a number, merge with an equal number to the right if possible
236
	local hadMove = false
237
	local tempC
238
	
239
	for r = 0, 3 do --For each row
240
		for c = 0, 3 do --For each column in row r
241
			if grid[r][c] == 0 then
242
				--Find the next non-zero and shift it here
243
				tempC = c + 1
244
				while tempC < 4 and grid[r][tempC] == 0 do
245
					tempC = tempC + 1
246
				end
247
				if tempC < 4 then
248
					hadMove = true
249
					grid[r][c] = grid[r][tempC]
250
					grid[r][tempC] = 0
251
				end
252
			end
253
			--Check for merges
254
			--If the next non-zero in the row matches this cell then merge
255
			tempC = c + 1
256
			while tempC < 4 and grid[r][tempC] == 0 do
257
				tempC = tempC + 1
258
			end
259
			if grid[r][c] == grid[r][tempC] then
260
				hadMove = true
261
				score = score + grid[r][c] + grid[r][tempC]
262
				grid[r][c] = grid[r][c] + grid[r][tempC]
263
				grid[r][tempC] = 0
264
			end
265
		end
266
	end
267
	
268
	return hadMove
269
end
270
271
function isGameOver()
272
	--If there are any zeroes in the grid then there's a valid move
273
	--Otherwise, if the cell to the right or underneath the current
274
	--	cell has the same value then a merge is possible.
275
	local r = 0
276
	while r < 4 do
277
		local c = 0
278
		while c < 4 do
279
			if grid[r][c] == 0 then
280
				return false
281
			elseif r < 3 and grid[r][c] == grid[r+1][c] then
282
				return false
283
			elseif c < 3 and grid[r][c] == grid[r][c+1] then
284
				return false
285
			end
286
			c = c + 1
287
		end
288
		r = r + 1
289
	end
290
	
291
	return true
292
end
293
 
294
function moveLeft()
295
	return collapseLeft()
296
end
297
 
298
function moveRight()
299
	--Mirror the grid so it can be collapsed to the left, then mirror it back
300
	reverseRows()
301
	hadMove = collapseLeft()
302
	reverseRows()
303
	return hadMove
304
end
305
306
function moveDown()
307
	--Rotate the grid so it's down becomes left, collapse, revert
308
	rotateClockwise()
309
	hadMove = collapseLeft()
310
	rotateCounterClockwise()
311
	return hadMove
312
end
313
314
function moveUp()
315
	--Rotate the grid so it's up becomes left, collapse, revert
316
	rotateCounterClockwise()
317
	hadMove = collapseLeft()
318
	rotateClockwise()
319
	return hadMove
320
end
321
322
function introShiftColumns(column, counter)
323
	--Weird, hacky code to animate the intro sequence.
324
	counter = counter - column
325
	if counter >= 0 and counter < 3 then
326
		grid[counter + 1][column] = grid[counter][column]
327
		grid[counter][column] = 0
328
	elseif counter >= 0 and counter < 5 then
329
		grid[5 - counter][column] = grid[5 - counter + 1][column]
330
		grid[5 - counter + 1][column] = 0		
331
	end
332
end
333
 
334
function intro()
335
	--This is a bit ugly
336
	clearGrid()
337
	for counter = 0, 8 do
338
		if counter == 0 then
339
			grid[0][0] = 2
340
		elseif counter == 1 then
341
			grid[0][1] = -1
342
		elseif counter == 2 then
343
			grid[0][2] = 4
344
		elseif counter == 3 then
345
			grid[0][3] = 8
346
		end
347
		drawScreen(true)
348
		sleep(ANIMATION_TIME)
349
		for i = 0, 3 do
350
			introShiftColumns(i, counter)
351
		end
352
	end
353
	--Color cycle
354
	for counter = 0, 10 do
355
		drawSquare(1, 0, 2, true)
356
		drawSquare(1, 1, -1, true)
357
		drawSquare(1, 2, 4, true)
358
		drawSquare(1, 3, 8, true)
359
		sleep(ANIMATION_TIME)
360
	end
361
	m.setBackgroundColor(colors.black)
362
	if isValidMonitorConnected then
363
		m.setCursorPos(termWidth / 2 - string.len("Touch to begin") / 2 + 1, tlGridY + 7)
364
		m.write("Touch to begin")
365
		m.setCursorPos(tlGridX + 1, tlGridY + 9)
366
		m.write("Controls: Click")
367
		m.setCursorPos(tlGridX + 1, tlGridY + 10)
368
		m.write("a side to move.")
369-
		_, _, x, y = os.pullEvent("monitor_touch") --Wait for click after intro
369+
		_, _, x, y = sapphUtils.pullEvent("monitor_touch") --Wait for click after intro
370
		if x == termWidth and y == 1 then --If quit was pressed
371-
			error() --Ghetto quit 
371+
		while true do sapphUtils.pullEvent() end	 --Ghetto quit
372
		end
373
	else
374
		m.setCursorPos(termWidth / 2 - string.len("Press any key...") / 2 + 1, tlGridY + 7)
375
		m.write("Press any key...")		
376-
		_, keycode = os.pullEvent("key") --Wait for keypress after intro
376+
		_, keycode = sapphUtils.pullEvent("key") --Wait for keypress after intro
377
		if keys.getName(keycode) == "x" then --If quit was pressed
378
			m.setBackgroundColor(colors.black)
379
			m.clear()
380-
			error() --Ghetto quit 
380+
			while true do sapphUtils.pullEvent() end --Ghetto quit 
381
		end
382
	end
383
end
384
		
385
 --
386
 --MAIN GAME LOOP
387
 --
388
grid = {} --Main 2D array for the game grid
389
score = 0 --Score is the sum of all merges performed during a game
390
391
isValidMonitorConnected = getMonitor() --Detect a monitor for output instead of the term if one is connected
392
if isValidMonitorConnected then
393
	termWidth, termHeight = m.getSize()
394
	tlGridX = termWidth / 2 - 7 --Top left X coord of the game grid (not including borders)
395
	tlGridY = 3 --Top left Y coord of the game grid (not including borders)
396
else
397
	m = term --Use the computer's terminal as output
398
	termWidth, termHeight = term.getSize() 
399
	tlGridX = termWidth / 2 - 7
400
	tlGridY = 3
401
end
402
403
if not m.isColor() then --Color monitors/computers only
404
	print("An advanced monitor or computer is required.")
405
	print("Press any key to exit...")
406-
	os.pullEvent("key")
406+
	sapphUtils.pullEvent("key")
407
	return
408
end
409
410
while true do
411
	intro() --Comment this out to disable the intro.  I won't judge
412
	clearGrid() --Initialize the game board array
413
	
414
	--Place the initial two tiles
415
	score = 0
416
	placeNewTile()
417
	placeNewTile()
418
	drawScreen()
419
		
420
	--Begin main round loop
421
	gameOver = false
422
	hadMove = false
423
	while not gameOver do
424
		hadMove = false
425
		if isValidMonitorConnected then --Use touch if a monitor is being used
426
			--Detect a click event for the buttons
427-
			_, _, xT, yT = os.pullEvent("monitor_touch")
427+
			_, _, xT, yT = sapphUtils.pullEvent("monitor_touch")
428
			--If a direction button was pressed
429
			if yT >= tlGridY - 1 and yT <= tlGridY + 1 and xT >= tlGridX + 4 and xT <= tlGridX + 11 then
430
				hadMove = moveUp()
431
			elseif yT >= tlGridY + 9 and yT <= tlGridY + 12 and xT >= tlGridX + 4 and xT <= tlGridX + 11 then
432
				hadMove = moveDown()
433
			elseif yT >= tlGridY + 3 and yT <= tlGridY + 8 and xT >= tlGridX - 1 and xT <= tlGridX + 3 then
434
				hadMove = moveLeft()
435
			elseif yT >= tlGridY + 3 and yT <= tlGridY + 8 and xT >= tlGridX + 12 and xT <= tlGridX + 16 then					
436
				hadMove = moveRight()
437
			elseif yT == CONTROLS_Y and xT >= (termWidth / 2 - string.len("RESET") / 2 + 1) and xT <= termWidth / 2 - string.len("RESET") / 2 + 1 + string.len("RESET") then
438
				hadMove = false
439
				gameOver = true --Reset button hit
440
			elseif yT == 1 and xT == termWidth then
441
				m.setBackgroundColor(colors.black)
442
				m.clear()
443
				return --Quit
444
			end
445
		else --Use keyboard controls if playing directly on a computer
446
			_, keycode = os.pullEvent("key")
447
			if keys.getName(keycode) == "up" then
448
				hadMove = moveUp()
449
			elseif keys.getName(keycode) == "down" then
450
				hadMove = moveDown()
451
			elseif keys.getName(keycode) == "left" then
452
				hadMove = moveLeft()
453
			elseif keys.getName(keycode) == "right" then
454
				hadMove = moveRight()
455
			elseif keys.getName(keycode) == "r" then --Reset
456
				hadMove = false	
457
				gameOver = true
458
			elseif keys.getName(keycode) == "x" then --Quit
459
				m.setBackgroundColor(colors.black)
460
				m.clear()
461
				m.setCursorPos(1,1)
462
				return --Quit
463
			end
464
		end
465
		
466
		if hadMove then
467
			placeNewTile()
468
			drawGrid()
469
			drawScore()
470
		end
471
472
		if isGameOver() then
473
			gameOver = true
474
		end
475
	end
476
	
477
	--Draw game over
478
	m.setTextColor(colors.white)
479
	m.setBackgroundColor(colors.red)
480
	m.setCursorPos(termWidth / 2 - string.len("GAME OVER") / 2 + 1, CONTROLS_Y - 1)
481
	m.write("GAME OVER")
482
	m.setBackgroundColor(colors.black)
483
	if isValidMonitorConnected then
484
		m.setCursorPos(termWidth / 2 - string.len("Touch to reset...") / 2 + 1, CONTROLS_Y)
485
		m.write("Touch to reset...")
486
		--Wait for any click to continue
487-
		os.pullEvent("monitor_touch")
487+
		sapphUtils.pullEvent("monitor_touch")
488
	else
489
		m.setCursorPos(termWidth / 2 - string.len("Press any key...") / 2 + 1, CONTROLS_Y)
490
		m.write("Press any key...")
491
		--Wait for any click to continue
492-
		os.pullEvent("key")
492+
		sapphUtils.pullEvent("key")
493
	end
494
end
495
while true do
496
  sapphUtils.pullEvent()
497
--keeps sapphari on "life support"
498
end