diff options
Diffstat (limited to 'apps/plugins/pixel-painter.lua')
-rw-r--r-- | apps/plugins/pixel-painter.lua | 701 |
1 files changed, 701 insertions, 0 deletions
diff --git a/apps/plugins/pixel-painter.lua b/apps/plugins/pixel-painter.lua new file mode 100644 index 0000000000..d2bd700d74 --- /dev/null +++ b/apps/plugins/pixel-painter.lua | |||
@@ -0,0 +1,701 @@ | |||
1 | --[[ | ||
2 | __________ __ ___. | ||
3 | Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | \/ \/ \/ \/ \/ | ||
8 | $Id$ | ||
9 | |||
10 | Port and extension of Pixel Painter by Pavel Bakhilau | ||
11 | (http://js1k.com/2010-first/demo/453) to Rockbox Lua. | ||
12 | |||
13 | Copyright (C) 2011 by Stefan Schneider-Kennedy | ||
14 | |||
15 | This program is free software; you can redistribute it and/or | ||
16 | modify it under the terms of the GNU General Public License | ||
17 | as published by the Free Software Foundation; either version 2 | ||
18 | of the License, or (at your option) any later version. | ||
19 | |||
20 | This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
21 | KIND, either express or implied. | ||
22 | |||
23 | ]]-- | ||
24 | |||
25 | --Number of colours used in the game | ||
26 | --Hard coded here, in the COLOURS and PIP_COLOURS definitions and in | ||
27 | --get_colours_count's count_table | ||
28 | NUM_COLOURS = 6 | ||
29 | |||
30 | --Utility function makes a copy of the passed table | ||
31 | function deepcopy(object) | ||
32 | local lookup_table = {} | ||
33 | local function _copy(object) | ||
34 | if type(object) ~= "table" then | ||
35 | return object | ||
36 | elseif lookup_table[object] then | ||
37 | return lookup_table[object] | ||
38 | end | ||
39 | local new_table = {} | ||
40 | lookup_table[object] = new_table | ||
41 | for index, value in pairs(object) do | ||
42 | new_table[_copy(index)] = _copy(value) | ||
43 | end | ||
44 | return setmetatable(new_table, getmetatable(object)) | ||
45 | end | ||
46 | return _copy(object) | ||
47 | end | ||
48 | |||
49 | --Returns the maximum value of the passed table and its index | ||
50 | function table_maximum(a) | ||
51 | local mi = 1 | ||
52 | local m = a[mi] | ||
53 | for i, val in ipairs(a) do | ||
54 | if val > m then | ||
55 | mi = i | ||
56 | m = val | ||
57 | end | ||
58 | end | ||
59 | return m, mi | ||
60 | end | ||
61 | |||
62 | --Solves the board using a simple algorithm and returns the number of | ||
63 | --moves required. Each turn, the function picks the move which fills in | ||
64 | --the greatest area of board. The number of moves required to complete | ||
65 | --it is returned. | ||
66 | function calculate_par(board) | ||
67 | local board_dimension = table.getn(board) | ||
68 | local test_game_copy = deepcopy(board) | ||
69 | local moves = 0 | ||
70 | |||
71 | repeat | ||
72 | local non_matching = {} | ||
73 | fill_board(test_game_copy, 0, 1, 1, test_game_copy[1][1], non_matching) | ||
74 | |||
75 | if table.getn(non_matching) > 0 then | ||
76 | local count_table = get_colours_count(test_game_copy, non_matching) | ||
77 | local max_value, colour = table_maximum(count_table) | ||
78 | |||
79 | --Corrects the invalid colour values set by | ||
80 | --get_colours_count, this also acts as a move | ||
81 | for x=1,board_dimension do | ||
82 | for y=1,board_dimension do | ||
83 | if test_game_copy[x][y] < 0 then | ||
84 | test_game_copy[x][y] = test_game_copy[x][y] * -1 | ||
85 | elseif test_game_copy[x][y] == 0 then | ||
86 | test_game_copy[x][y] = colour | ||
87 | end | ||
88 | end | ||
89 | end | ||
90 | else | ||
91 | return moves | ||
92 | end | ||
93 | --Manual garbage collection is needed so it doesn't eat into the | ||
94 | --audio buffer, see http://forums.rockbox.org/index.php/topic,27120.msg177434.html | ||
95 | collectgarbage("collect") | ||
96 | moves = moves + 1 | ||
97 | until false | ||
98 | end | ||
99 | |||
100 | --Calculates the number of blocks of each colour adjacent to the filled | ||
101 | --region identified by the passed parameters. A colour indexed table | ||
102 | --containing the counts is returned. Relies on the board having been | ||
103 | --flood filled with 0s prior to executing this function. | ||
104 | -- | ||
105 | --The 'board' table is also adjusted as follows: The filled region's | ||
106 | --colour index is set to zero and each of the adjacent areas' colour | ||
107 | --indexes are multiplied by -1. These invalid colour values are later | ||
108 | --corrected in the calculate_par function. | ||
109 | function get_colours_count(board, non_matching) | ||
110 | local count_table = {0, 0, 0, 0, 0, 0} | ||
111 | repeat | ||
112 | --Pop the array | ||
113 | local current = non_matching[table.getn(non_matching)] | ||
114 | table.remove(non_matching) | ||
115 | --Check this square hasn't already been filled | ||
116 | local curr_colour = board[current[1]][current[2]] | ||
117 | if curr_colour > 0 then | ||
118 | count_table[curr_colour] = count_table[curr_colour] + | ||
119 | fill_board(board, curr_colour * -1, current[1], current[2], curr_colour) | ||
120 | end | ||
121 | until table.getn(non_matching) == 0 | ||
122 | |||
123 | return count_table | ||
124 | end | ||
125 | |||
126 | --Returns a randomly coloured board of the indicated dimensions | ||
127 | function generate_board(board_dimension, seed) | ||
128 | math.randomseed(seed) | ||
129 | |||
130 | local board = {} | ||
131 | for x=1,board_dimension do | ||
132 | board[x] = {} | ||
133 | for y=1,board_dimension do | ||
134 | board[x][y] = math.random(1,NUM_COLOURS) | ||
135 | end | ||
136 | end | ||
137 | |||
138 | return board | ||
139 | end | ||
140 | |||
141 | --Flood fills the board from the top left using selected_colour | ||
142 | --Returns the number of boxes filled | ||
143 | function fill_board(board, fill_colour, x, y, original_colour, non_matching) | ||
144 | local board_dimension = table.getn(board) | ||
145 | if x > 0 and y > 0 and x <= board_dimension and y <= board_dimension then | ||
146 | if board[x][y] == original_colour then | ||
147 | board[x][y] = fill_colour | ||
148 | return fill_board(board, fill_colour, x - 1, y, original_colour, non_matching) + | ||
149 | fill_board(board, fill_colour, x, y - 1, original_colour, non_matching) + | ||
150 | fill_board(board, fill_colour, x + 1, y, original_colour, non_matching) + | ||
151 | fill_board(board, fill_colour, x, y + 1, original_colour, non_matching) + 1 | ||
152 | elseif(non_matching ~= nil and board[x][y] ~= fill_colour) then | ||
153 | table.insert(non_matching, {x,y}) | ||
154 | end | ||
155 | end | ||
156 | |||
157 | return 0 | ||
158 | end | ||
159 | |||
160 | --Checks whether the given board is a single colour | ||
161 | function check_win(board) | ||
162 | for x,col in pairs(board) do | ||
163 | for y,value in pairs(col) do | ||
164 | if value ~= board[1][1] then | ||
165 | return false | ||
166 | end | ||
167 | end | ||
168 | end | ||
169 | |||
170 | return true | ||
171 | end | ||
172 | |||
173 | --Attempt to load the game variables stored in the indicated save file. | ||
174 | --Returns a table containing game variables if the file can be opened, | ||
175 | --false otherwise. | ||
176 | --Table keys are: difficulty, par, move_number, selected_colour, board | ||
177 | function load_game(filename) | ||
178 | local f = io.open(filename, "r") | ||
179 | if f ~= nil then | ||
180 | local rtn = {} | ||
181 | rtn["difficulty"] = tonumber(f:read()) | ||
182 | rtn["par"] = tonumber(f:read()) | ||
183 | rtn["move_number"] = tonumber(f:read()) | ||
184 | rtn["selected_colour"] = tonumber(f:read()) | ||
185 | |||
186 | local board={} | ||
187 | local dimension = diff_to_dimension(rtn["difficulty"]) | ||
188 | for x=1,dimension do | ||
189 | board[x] = {} | ||
190 | local line = f:read() | ||
191 | local bits = {line:match(("([^ ]*) "):rep(dimension))} | ||
192 | for y=1,dimension do | ||
193 | board[x][y] = tonumber(bits[y]) | ||
194 | end | ||
195 | end | ||
196 | rtn["board"] = board | ||
197 | |||
198 | f:close() | ||
199 | return rtn | ||
200 | else | ||
201 | return false | ||
202 | end | ||
203 | end | ||
204 | |||
205 | --Saves the game state to file | ||
206 | function save_game(state, filename) | ||
207 | local f = io.open(filename, "w") | ||
208 | if f ~= nil then | ||
209 | f:write(state["difficulty"],"\n") | ||
210 | f:write(state["par"],"\n") | ||
211 | f:write(state["move_number"],"\n") | ||
212 | f:write(state["selected_colour"],"\n") | ||
213 | local board = state["board"] | ||
214 | local dimension = diff_to_dimension(state["difficulty"]) | ||
215 | for x=1,dimension do | ||
216 | for y=1,dimension do | ||
217 | f:write(board[x][y]," ") | ||
218 | end | ||
219 | f:write("\n") | ||
220 | end | ||
221 | f:close() | ||
222 | return true | ||
223 | else | ||
224 | return false | ||
225 | end | ||
226 | end | ||
227 | |||
228 | --Loads the high scores from file | ||
229 | --Returns true on success, false otherwise | ||
230 | function load_scores(filename) | ||
231 | local f = io.open(filename, "r") | ||
232 | if f ~= nil then | ||
233 | local highscores = {} | ||
234 | for i=1,3 do | ||
235 | local line = f:read() | ||
236 | local value = false | ||
237 | if line ~= nil then | ||
238 | value = tonumber(line) | ||
239 | end | ||
240 | |||
241 | highscores[i] = value | ||
242 | end | ||
243 | f:close() | ||
244 | return highscores | ||
245 | else | ||
246 | return false | ||
247 | end | ||
248 | end | ||
249 | |||
250 | --Saves the high scores to file | ||
251 | function save_scores(highscores, filename) | ||
252 | local f = io.open(filename, "w") | ||
253 | if f ~= nil then | ||
254 | for i=1,3 do | ||
255 | local value = highscores[i] | ||
256 | if value == false then | ||
257 | value = "" | ||
258 | end | ||
259 | f:write(value,"\n") | ||
260 | end | ||
261 | f:close() | ||
262 | return true | ||
263 | else | ||
264 | return false | ||
265 | end | ||
266 | end | ||
267 | |||
268 | function diff_to_dimension(difficulty) | ||
269 | if difficulty == 1 then | ||
270 | return 8 | ||
271 | elseif difficulty == 2 then | ||
272 | return 16 | ||
273 | else | ||
274 | return 22 | ||
275 | end | ||
276 | end | ||
277 | |||
278 | --Don't run the RB stuff if we're not running under RB | ||
279 | if rb ~= nil then | ||
280 | |||
281 | if rb.lcd_rgbpack == nil then | ||
282 | rb.splash(2*rb.HZ, "Non RGB targets not currently supported") | ||
283 | os.exit() | ||
284 | end | ||
285 | |||
286 | --------------------- | ||
287 | --RB Game variables-- | ||
288 | --------------------- | ||
289 | |||
290 | --The colours used by the game | ||
291 | COLOURS = { | ||
292 | rb.lcd_rgbpack(255, 119, 34), | ||
293 | rb.lcd_rgbpack(255, 255, 102), | ||
294 | rb.lcd_rgbpack(119, 204, 51), | ||
295 | rb.lcd_rgbpack(102, 170, 255), | ||
296 | rb.lcd_rgbpack(51, 68, 255), | ||
297 | rb.lcd_rgbpack(51, 51, 51), | ||
298 | } | ||
299 | --The colour of the selection pip | ||
300 | PIP_COLOURS = { | ||
301 | rb.lcd_rgbpack(0, 0, 0), | ||
302 | rb.lcd_rgbpack(0, 0, 0), | ||
303 | rb.lcd_rgbpack(0, 0, 0), | ||
304 | rb.lcd_rgbpack(0, 0, 0), | ||
305 | rb.lcd_rgbpack(0, 0, 0), | ||
306 | rb.lcd_rgbpack(255, 255, 255), | ||
307 | } | ||
308 | DEFAULT_DIFFICULTY = 2 --1: Easy, 2: Normal, 3: Hard | ||
309 | |||
310 | FILES_ROOT = "/.rockbox/rocks/games/" | ||
311 | SCORES_FILE = FILES_ROOT.."pixel-painter.score" | ||
312 | SAVE_FILE = FILES_ROOT.."pixel-painter.save" | ||
313 | r,w,TEXT_LINE_HEIGHT = rb.font_getstringsize(" ", rb.FONT_UI) --Get font height | ||
314 | --Determine which layout to use by considering the screen dimensions | ||
315 | --the +9 is so we have space for the chooser | ||
316 | if rb.LCD_WIDTH > (rb.LCD_HEIGHT + 9) then | ||
317 | LAYOUT = 1 --Wider than high, status and chooser on right | ||
318 | elseif rb.LCD_HEIGHT > (rb.LCD_WIDTH + 9) then | ||
319 | LAYOUT = 2 --Higher than wide, status and chooser below | ||
320 | else | ||
321 | LAYOUT = 3 --Treat like a square screen, chooser on right, status below | ||
322 | end | ||
323 | |||
324 | --Display variables | ||
325 | chooser_pip_dimension = 6 --pixel dimension of the selected colour pip | ||
326 | block_width = 0 --pixel dimension of each game square | ||
327 | chooser_start_pos = 0 --x or y position of the first block (depending on LAYOUT) | ||
328 | |||
329 | --Populated by load_scores() | ||
330 | highscores = {false, false, false} | ||
331 | |||
332 | --A table containing the game state, initialised by init_game() or | ||
333 | --load_game(), see | ||
334 | game_state = {} | ||
335 | |||
336 | ----------------------------------- | ||
337 | --Display and interface functions-- | ||
338 | ----------------------------------- | ||
339 | |||
340 | --Sets up a new game and display variables for the indicated | ||
341 | --difficulty | ||
342 | function init_game(difficulty) | ||
343 | init_display_variables(difficulty) | ||
344 | local state = {} | ||
345 | local board_dimension = diff_to_dimension(difficulty) | ||
346 | state["selected_colour"] = 1 | ||
347 | state["move_number"] = 0 | ||
348 | state["difficulty"] = difficulty | ||
349 | state["board"] = generate_board(board_dimension, rb.current_tick()+os.time()) | ||
350 | rb.splash(1, "Calculating par...") --Will stay on screen until it's done | ||
351 | state["par"] = calculate_par(state["board"]) | ||
352 | |||
353 | return state | ||
354 | end | ||
355 | |||
356 | --Initialises the display variables for the screen LAYOUT | ||
357 | function init_display_variables(difficulty) | ||
358 | local board_dimension = diff_to_dimension(difficulty) | ||
359 | |||
360 | if LAYOUT == 1 then | ||
361 | block_width = rb.LCD_HEIGHT / board_dimension | ||
362 | chooser_start_pos = (board_dimension)*block_width + 2 | ||
363 | chooser_width = rb.LCD_WIDTH - chooser_start_pos | ||
364 | chooser_height = (rb.LCD_HEIGHT - 3*TEXT_LINE_HEIGHT) / NUM_COLOURS | ||
365 | elseif LAYOUT == 2 then | ||
366 | block_width = rb.LCD_WIDTH / board_dimension | ||
367 | chooser_start_pos = board_dimension*block_width + 2 + TEXT_LINE_HEIGHT | ||
368 | chooser_width = rb.LCD_WIDTH / NUM_COLOURS | ||
369 | chooser_height = rb.LCD_HEIGHT - chooser_start_pos | ||
370 | else | ||
371 | if TEXT_LINE_HEIGHT > 9 then | ||
372 | block_width = (rb.LCD_HEIGHT - TEXT_LINE_HEIGHT) / board_dimension | ||
373 | else | ||
374 | block_width = (rb.LCD_HEIGHT - 9) / board_dimension | ||
375 | end | ||
376 | chooser_start_pos = (board_dimension)*block_width + 1 | ||
377 | chooser_width = rb.LCD_WIDTH - chooser_start_pos | ||
378 | chooser_height = (rb.LCD_HEIGHT - TEXT_LINE_HEIGHT) / NUM_COLOURS | ||
379 | end | ||
380 | end | ||
381 | |||
382 | --Draws the game board to screen | ||
383 | function draw_board(board) | ||
384 | for x,col in pairs(board) do | ||
385 | for y,value in pairs(col) do | ||
386 | rb.lcd_set_foreground(COLOURS[value]) | ||
387 | rb.lcd_fillrect((x-1)*block_width, (y-1)*block_width, block_width, block_width) | ||
388 | end | ||
389 | end | ||
390 | end | ||
391 | |||
392 | --Draw the colour chooser along with selected pip at the appropriate | ||
393 | --position | ||
394 | function draw_chooser(selected_colour) | ||
395 | for i=1,NUM_COLOURS do | ||
396 | rb.lcd_set_foreground(COLOURS[i]) | ||
397 | if LAYOUT == 1 or LAYOUT == 3 then | ||
398 | rb.lcd_fillrect(chooser_start_pos, (i - 1)*(chooser_height), chooser_width, chooser_height) | ||
399 | elseif LAYOUT == 2 then | ||
400 | rb.lcd_fillrect((i - 1)*(chooser_width), chooser_start_pos, chooser_width, chooser_height) | ||
401 | end | ||
402 | end | ||
403 | |||
404 | rb.lcd_set_foreground(PIP_COLOURS[selected_colour]) | ||
405 | local xpos = 0 | ||
406 | local ypos = 0 | ||
407 | if LAYOUT == 1 or LAYOUT == 3 then | ||
408 | xpos = chooser_start_pos + (chooser_width - chooser_pip_dimension)/2 | ||
409 | ypos = (selected_colour-1)*(chooser_height) + (chooser_height - chooser_pip_dimension)/2 | ||
410 | elseif LAYOUT == 2 then | ||
411 | xpos = (selected_colour-1)*(chooser_width) + (chooser_width - chooser_pip_dimension)/2 | ||
412 | ypos = chooser_start_pos + (chooser_height - chooser_pip_dimension)/2 | ||
413 | end | ||
414 | rb.lcd_fillrect(xpos, ypos, chooser_pip_dimension, chooser_pip_dimension) | ||
415 | end | ||
416 | |||
417 | --Draws the current moves, par and high score | ||
418 | function draw_status(move_number, par, highscore) | ||
419 | local strings = {"Move", move_number, "Par", par} | ||
420 | if highscore then | ||
421 | table.insert(strings, "Best") | ||
422 | table.insert(strings, highscore) | ||
423 | end | ||
424 | |||
425 | if LAYOUT == 1 then | ||
426 | local function calc_string(var_len_str, static_str) | ||
427 | local avail_width = chooser_width - rb.font_getstringsize(static_str, rb.FONT_UI) | ||
428 | local rtn_str = "" | ||
429 | |||
430 | for i=1,string.len(var_len_str) do | ||
431 | local c = string.sub(var_len_str, i, i) | ||
432 | local curr_width = rb.font_getstringsize(rtn_str, rb.FONT_UI) | ||
433 | local width = rb.font_getstringsize(c, rb.FONT_UI) | ||
434 | |||
435 | if curr_width + width <= avail_width then | ||
436 | rtn_str = rtn_str .. c | ||
437 | else | ||
438 | break | ||
439 | end | ||
440 | end | ||
441 | |||
442 | return rtn_str, rb.font_getstringsize(rtn_str, rb.FONT_UI) | ||
443 | end | ||
444 | |||
445 | local height = NUM_COLOURS*chooser_height | ||
446 | colon_width = rb.font_getstringsize(": ", rb.FONT_UI) | ||
447 | for i = 1,table.getn(strings),2 do | ||
448 | local label, label_width = calc_string(strings[i], ": "..strings[i+1]) | ||
449 | |||
450 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0)) | ||
451 | rb.lcd_putsxy(chooser_start_pos, height, label..": ") | ||
452 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255)) | ||
453 | rb.lcd_putsxy(chooser_start_pos + label_width + colon_width, height, strings[i+1]) | ||
454 | height = height + TEXT_LINE_HEIGHT | ||
455 | end | ||
456 | else | ||
457 | local text_ypos = 0 | ||
458 | if LAYOUT == 2 then | ||
459 | text_ypos = chooser_start_pos - TEXT_LINE_HEIGHT - 1 | ||
460 | else | ||
461 | text_ypos = rb.LCD_HEIGHT - TEXT_LINE_HEIGHT | ||
462 | end | ||
463 | space_width = rb.font_getstringsize(" ", rb.FONT_UI) | ||
464 | local xpos = 0 | ||
465 | for i = 1,table.getn(strings),2 do | ||
466 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0)) | ||
467 | rb.lcd_putsxy(xpos, text_ypos, strings[i]..": ") | ||
468 | xpos = xpos + rb.font_getstringsize(strings[i]..": ", rb.FONT_UI) | ||
469 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255)) | ||
470 | rb.lcd_putsxy(xpos, text_ypos, strings[i+1]) | ||
471 | xpos = xpos + rb.font_getstringsize(strings[i+1], rb.FONT_UI) + space_width | ||
472 | end | ||
473 | end | ||
474 | end | ||
475 | |||
476 | --Convenience function to redraw the whole board to screen | ||
477 | function redraw_game(game_state, highscores) | ||
478 | rb.lcd_clear_display() | ||
479 | draw_board(game_state["board"]) | ||
480 | draw_chooser(game_state["selected_colour"]) | ||
481 | draw_status(game_state["move_number"], game_state["par"], | ||
482 | highscores[game_state["difficulty"]]) | ||
483 | rb.lcd_update() | ||
484 | end | ||
485 | |||
486 | |||
487 | --Draws help to screen, waits for a keypress to exit | ||
488 | function app_help() | ||
489 | rb.lcd_clear_display() | ||
490 | |||
491 | local title = "Pixel painter help" | ||
492 | local rtn, title_width, h = rb.font_getstringsize(title, rb.FONT_UI) | ||
493 | local title_xpos = (rb.LCD_WIDTH - title_width) / 2 | ||
494 | local space_width = rb.font_getstringsize(" ", rb.FONT_UI) | ||
495 | |||
496 | --Draw title | ||
497 | function draw_text(y_offset) | ||
498 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0)) | ||
499 | rb.lcd_putsxy(title_xpos, y_offset, title) | ||
500 | rb.lcd_hline(title_xpos, title_xpos + title_width, TEXT_LINE_HEIGHT + y_offset) | ||
501 | rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255)) | ||
502 | |||
503 | local body_text = [[ | ||
504 | The aim is to fill the screen with a single colour. Each move you select a new colour which is then filled in from the top left corner. | ||
505 | |||
506 | The bottom right displays the number of moves taken, the number of moves used by the computer and your best score relative to the computer's. | ||
507 | ]] | ||
508 | local body_len = string.len(body_text) | ||
509 | |||
510 | --Draw body text | ||
511 | local word_buffer = "" | ||
512 | local xpos = 0 | ||
513 | local ypos = TEXT_LINE_HEIGHT * 2 | ||
514 | for i=1,body_len do | ||
515 | local c = string.sub(body_text, i, i) | ||
516 | if c == " " or c == "\n" then | ||
517 | local word_length = rb.font_getstringsize(word_buffer, rb.FONT_UI) | ||
518 | if (xpos + word_length) > rb.LCD_WIDTH then | ||
519 | xpos = 0 | ||
520 | ypos = ypos + TEXT_LINE_HEIGHT | ||
521 | end | ||
522 | rb.lcd_putsxy(xpos, ypos + y_offset, word_buffer) | ||
523 | |||
524 | word_buffer = "" | ||
525 | if c == "\n" then | ||
526 | xpos = 0 | ||
527 | ypos = ypos + TEXT_LINE_HEIGHT | ||
528 | else | ||
529 | xpos = xpos + word_length + space_width | ||
530 | end | ||
531 | else | ||
532 | word_buffer = word_buffer .. c | ||
533 | end | ||
534 | end | ||
535 | |||
536 | rb.lcd_update() | ||
537 | |||
538 | return ypos | ||
539 | end | ||
540 | |||
541 | --Deal with scrolling the help | ||
542 | local y_offset = 0 | ||
543 | local max_y_offset = math.max(draw_text(y_offset) - rb.LCD_HEIGHT, 0) | ||
544 | local exit = false | ||
545 | repeat | ||
546 | local action = rb.get_plugin_action(0) | ||
547 | if action == rb.actions.PLA_DOWN then | ||
548 | y_offset = math.max(-max_y_offset, y_offset - TEXT_LINE_HEIGHT) | ||
549 | elseif action == rb.actions.PLA_UP then | ||
550 | y_offset = math.min(0, y_offset + TEXT_LINE_HEIGHT) | ||
551 | elseif action == rb.actions.PLA_LEFT or | ||
552 | action == rb.actions.PLA_RIGHT or | ||
553 | action == rb.actions.PLA_SELECT or | ||
554 | action == rb.actions.PLA_EXIT or | ||
555 | action == rb.actions.PLA_CANCEL then | ||
556 | --This explicit enumeration is needed for targets like | ||
557 | --the iriver which send more than one action when | ||
558 | --scrolling | ||
559 | |||
560 | exit = true | ||
561 | end | ||
562 | rb.lcd_clear_display() | ||
563 | draw_text(y_offset) | ||
564 | until exit == true | ||
565 | end | ||
566 | |||
567 | --Draws the application menu and handles its logic | ||
568 | function app_menu() | ||
569 | local options = {"Resume game", "Start new game", "Change difficulty", | ||
570 | "Help", "Quit without saving", "Quit"} | ||
571 | local item = rb.do_menu("Pixel painter menu", options, nil, false) | ||
572 | |||
573 | if item == 0 then | ||
574 | redraw_game(game_state, highscores) | ||
575 | elseif item == 1 then | ||
576 | os.remove(SAVE_FILE) | ||
577 | game_state = init_game(game_state["difficulty"]) | ||
578 | redraw_game(game_state, highscores) | ||
579 | elseif item == 2 then | ||
580 | local diff = rb.do_menu("Difficulty", {"Easy", "Medium", "Hard"}, game_state["difficulty"] - 1, false) | ||
581 | if diff < 0 then | ||
582 | app_menu() | ||
583 | else | ||
584 | local difficulty = diff + 1 --lua is 1 indexed | ||
585 | os.remove(SAVE_FILE) | ||
586 | game_state = init_game(difficulty) | ||
587 | redraw_game(game_state, highscores) | ||
588 | end | ||
589 | elseif item == 3 then | ||
590 | app_help() | ||
591 | redraw_game(game_state, highscores) | ||
592 | elseif item == 4 then | ||
593 | os.remove(SAVE_FILE) | ||
594 | os.exit() | ||
595 | elseif item == 5 then | ||
596 | rb.splash(1, "Saving game...") --Will stay on screen till the app exits | ||
597 | save_game(game_state,SAVE_FILE) | ||
598 | os.exit() | ||
599 | end | ||
600 | end | ||
601 | |||
602 | --Determine what victory text to show depending on the relation of the | ||
603 | --score to the calculated par value | ||
604 | function win_text(delta) | ||
605 | if delta < 0 then | ||
606 | return "You were "..(-1*delta).." under par" | ||
607 | elseif delta > 0 then | ||
608 | return "You were "..delta.." over par" | ||
609 | else | ||
610 | return "You attained par" | ||
611 | end | ||
612 | end | ||
613 | |||
614 | ------------------ | ||
615 | --Game main loop-- | ||
616 | ------------------ | ||
617 | |||
618 | --Gives the option of testing things without running the game, use: | ||
619 | --as_library=true | ||
620 | --dofile('pixel-painter.lua') | ||
621 | if not as_library then | ||
622 | game_state = load_game(SAVE_FILE) | ||
623 | if game_state then | ||
624 | init_display_variables(game_state["difficulty"]) | ||
625 | else | ||
626 | game_state = init_game(DEFAULT_DIFFICULTY) | ||
627 | end | ||
628 | loaded_scores = load_scores(SCORES_FILE) | ||
629 | if loaded_scores then | ||
630 | highscores = loaded_scores | ||
631 | end | ||
632 | redraw_game(game_state, highscores) | ||
633 | |||
634 | require("actions") | ||
635 | --Set the keys to use for scrolling the chooser | ||
636 | if LAYOUT == 1 or LAYOUT == 3 then -- landscape and square screens | ||
637 | prev_action = rb.actions.PLA_UP | ||
638 | prev_action_repeat = rb.actions.PLA_UP_REPEAT | ||
639 | next_action = rb.actions.PLA_DOWN | ||
640 | next_action_repeat = rb.actions.PLA_DOWN_REPEAT | ||
641 | else -- portrait screens | ||
642 | prev_action = rb.actions.PLA_LEFT | ||
643 | prev_action_repeat = rb.actions.PLA_LEFT_REPEAT | ||
644 | next_action = rb.actions.PLA_RIGHT | ||
645 | next_action_repeat = rb.actions.PLA_RIGHT_REPEAT | ||
646 | end | ||
647 | |||
648 | repeat | ||
649 | local action = rb.get_plugin_action(-1) -- TIMEOUT_BLOCK | ||
650 | |||
651 | if action == rb.actions.PLA_SELECT then | ||
652 | --Ensure the user has changed the colour before allowing move | ||
653 | --TODO: Check that the move would change the board | ||
654 | |||
655 | if game_state["selected_colour"] ~= game_state["board"][1][1] then | ||
656 | fill_board(game_state["board"], game_state["selected_colour"], | ||
657 | 1, 1, game_state["board"][1][1]) | ||
658 | game_state["move_number"] = game_state["move_number"] + 1 | ||
659 | redraw_game(game_state, highscores) | ||
660 | |||
661 | if check_win(game_state["board"]) then | ||
662 | local par_diff = game_state["move_number"] - game_state["par"] | ||
663 | if not highscores[game_state["difficulty"]] or | ||
664 | par_diff < highscores[game_state["difficulty"]] then | ||
665 | -- | ||
666 | rb.splash(3*rb.HZ, win_text(par_diff)..", a new high score!") | ||
667 | highscores[game_state["difficulty"]] = par_diff | ||
668 | save_scores(highscores, SCORES_FILE) | ||
669 | else | ||
670 | rb.splash(3*rb.HZ, win_text(par_diff)..".") | ||
671 | end | ||
672 | os.remove(SAVE_FILE) | ||
673 | os.exit() | ||
674 | end | ||
675 | else | ||
676 | --Will stay on screen until they move | ||
677 | rb.splash(1, "Invalid move (wouldn't change board). Change colour to continue.") | ||
678 | end | ||
679 | elseif action == next_action or action == next_action_repeat then | ||
680 | if game_state["selected_colour"] < NUM_COLOURS then | ||
681 | game_state["selected_colour"] = game_state["selected_colour"] + 1 | ||
682 | else | ||
683 | game_state["selected_colour"] = 1 | ||
684 | end | ||
685 | redraw_game(game_state, highscores) | ||
686 | elseif action == prev_action or action == prev_action_repeat then | ||
687 | if game_state["selected_colour"] > 1 then | ||
688 | game_state["selected_colour"] = game_state["selected_colour"] - 1 | ||
689 | else | ||
690 | game_state["selected_colour"] = NUM_COLOURS | ||
691 | end | ||
692 | redraw_game(game_state, highscores) | ||
693 | elseif action == rb.actions.PLA_CANCEL then | ||
694 | app_menu() | ||
695 | end | ||
696 | until action == rb.actions.PLA_EXIT | ||
697 | --This is executed if the user presses PLA_EXIT | ||
698 | rb.splash(1, "Saving game...") --Will stay on screen till the app exits | ||
699 | save_game(game_state,SAVE_FILE) | ||
700 | end | ||
701 | end | ||