summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastian Leonhardt <sebastian.leonhardt@web.de>2016-09-23 01:25:32 +0200
committerSebastian Leonhardt <sebastian.leonhardt@web.de>2017-08-23 00:25:11 +0200
commit28ae7004446b05f085083669182a81d1cc28c164 (patch)
tree01f3697619da51be5d1b37668df0edb1969d3bb7
parent7944969ff7576c0c87be22a6c361e0fa7c2283ed (diff)
downloadrockbox-28ae7004446b05f085083669182a81d1cc28c164.tar.gz
rockbox-28ae7004446b05f085083669182a81d1cc28c164.tar.bz2
rockbox-28ae7004446b05f085083669182a81d1cc28c164.zip
FS#11922: Lua game - Pixel Painter
* Changed keymaps to PLA and added to SOURCES and CATEGORIES file * improved keymaps: implement wrap-around and key repeat * change keymap according to screen orientation * fix font size calculation * use blocking button query in main loop * replace tabs with spaces * added manual entry * added original author to CREDITS Change-Id: Id67ae99cbb7a737c7f4608e278b77a389ac2ffa6
-rw-r--r--apps/plugins/CATEGORIES1
-rw-r--r--apps/plugins/SOURCES5
-rw-r--r--apps/plugins/pixel-painter.lua701
-rw-r--r--docs/CREDITS1
-rw-r--r--manual/plugins/images/ss-pixelpainter-128x128x16.pngbin0 -> 1181 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-128x160x16.pngbin0 -> 1139 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-128x96x16.pngbin0 -> 1065 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-132x80x16.pngbin0 -> 1029 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-160x128x16.pngbin0 -> 1158 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-176x132x16.pngbin0 -> 1314 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-176x220x16.pngbin0 -> 1387 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-220x176x16.pngbin0 -> 1563 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-240x320x16.pngbin0 -> 1686 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-240x400x16.pngbin0 -> 1838 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-320x240x16.pngbin0 -> 1708 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-320x240x24.pngbin0 -> 1708 bytes
-rw-r--r--manual/plugins/images/ss-pixelpainter-96x96x16.pngbin0 -> 995 bytes
-rw-r--r--manual/plugins/main.tex2
-rw-r--r--manual/plugins/pixelpainter.tex24
19 files changed, 733 insertions, 1 deletions
diff --git a/apps/plugins/CATEGORIES b/apps/plugins/CATEGORIES
index 929788f826..1b5bb9a87b 100644
--- a/apps/plugins/CATEGORIES
+++ b/apps/plugins/CATEGORIES
@@ -72,6 +72,7 @@ pegbox,games
periodic_table,apps
pictureflow,demos
pitch_detector,apps
+pixel-painter,games
plasma,demos
png,viewers
gif,viewers
diff --git a/apps/plugins/SOURCES b/apps/plugins/SOURCES
index b3a939695f..9eec04aa22 100644
--- a/apps/plugins/SOURCES
+++ b/apps/plugins/SOURCES
@@ -149,7 +149,10 @@ metronome.c
/* Lua needs at least 160 KB to work in */
#if PLUGIN_BUFFER_SIZE >= 0x80000
boomshine.lua
-#endif
+#ifdef HAVE_LCD_COLOR
+pixel-painter.lua
+#endif /* HAVE_LCD_COLOR */
+#endif /* PLUGIN_BUFFER_SIZE >= 0x80000 */
rockblox1d.c
brickmania.c
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 @@
+--[[
+ __________ __ ___.
+ Open \______ \ ____ ____ | | _\_ |__ _______ ___
+ Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
+ Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
+ Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
+ \/ \/ \/ \/ \/
+ $Id$
+
+ Port and extension of Pixel Painter by Pavel Bakhilau
+ (http://js1k.com/2010-first/demo/453) to Rockbox Lua.
+
+ Copyright (C) 2011 by Stefan Schneider-Kennedy
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+ KIND, either express or implied.
+
+]]--
+
+--Number of colours used in the game
+--Hard coded here, in the COLOURS and PIP_COLOURS definitions and in
+--get_colours_count's count_table
+NUM_COLOURS = 6
+
+--Utility function makes a copy of the passed table
+function deepcopy(object)
+ local lookup_table = {}
+ local function _copy(object)
+ if type(object) ~= "table" then
+ return object
+ elseif lookup_table[object] then
+ return lookup_table[object]
+ end
+ local new_table = {}
+ lookup_table[object] = new_table
+ for index, value in pairs(object) do
+ new_table[_copy(index)] = _copy(value)
+ end
+ return setmetatable(new_table, getmetatable(object))
+ end
+ return _copy(object)
+end
+
+--Returns the maximum value of the passed table and its index
+function table_maximum(a)
+ local mi = 1
+ local m = a[mi]
+ for i, val in ipairs(a) do
+ if val > m then
+ mi = i
+ m = val
+ end
+ end
+ return m, mi
+end
+
+--Solves the board using a simple algorithm and returns the number of
+--moves required. Each turn, the function picks the move which fills in
+--the greatest area of board. The number of moves required to complete
+--it is returned.
+function calculate_par(board)
+ local board_dimension = table.getn(board)
+ local test_game_copy = deepcopy(board)
+ local moves = 0
+
+ repeat
+ local non_matching = {}
+ fill_board(test_game_copy, 0, 1, 1, test_game_copy[1][1], non_matching)
+
+ if table.getn(non_matching) > 0 then
+ local count_table = get_colours_count(test_game_copy, non_matching)
+ local max_value, colour = table_maximum(count_table)
+
+ --Corrects the invalid colour values set by
+ --get_colours_count, this also acts as a move
+ for x=1,board_dimension do
+ for y=1,board_dimension do
+ if test_game_copy[x][y] < 0 then
+ test_game_copy[x][y] = test_game_copy[x][y] * -1
+ elseif test_game_copy[x][y] == 0 then
+ test_game_copy[x][y] = colour
+ end
+ end
+ end
+ else
+ return moves
+ end
+ --Manual garbage collection is needed so it doesn't eat into the
+ --audio buffer, see http://forums.rockbox.org/index.php/topic,27120.msg177434.html
+ collectgarbage("collect")
+ moves = moves + 1
+ until false
+end
+
+--Calculates the number of blocks of each colour adjacent to the filled
+--region identified by the passed parameters. A colour indexed table
+--containing the counts is returned. Relies on the board having been
+--flood filled with 0s prior to executing this function.
+--
+--The 'board' table is also adjusted as follows: The filled region's
+--colour index is set to zero and each of the adjacent areas' colour
+--indexes are multiplied by -1. These invalid colour values are later
+--corrected in the calculate_par function.
+function get_colours_count(board, non_matching)
+ local count_table = {0, 0, 0, 0, 0, 0}
+ repeat
+ --Pop the array
+ local current = non_matching[table.getn(non_matching)]
+ table.remove(non_matching)
+ --Check this square hasn't already been filled
+ local curr_colour = board[current[1]][current[2]]
+ if curr_colour > 0 then
+ count_table[curr_colour] = count_table[curr_colour] +
+ fill_board(board, curr_colour * -1, current[1], current[2], curr_colour)
+ end
+ until table.getn(non_matching) == 0
+
+ return count_table
+end
+
+--Returns a randomly coloured board of the indicated dimensions
+function generate_board(board_dimension, seed)
+ math.randomseed(seed)
+
+ local board = {}
+ for x=1,board_dimension do
+ board[x] = {}
+ for y=1,board_dimension do
+ board[x][y] = math.random(1,NUM_COLOURS)
+ end
+ end
+
+ return board
+end
+
+--Flood fills the board from the top left using selected_colour
+--Returns the number of boxes filled
+function fill_board(board, fill_colour, x, y, original_colour, non_matching)
+ local board_dimension = table.getn(board)
+ if x > 0 and y > 0 and x <= board_dimension and y <= board_dimension then
+ if board[x][y] == original_colour then
+ board[x][y] = fill_colour
+ return fill_board(board, fill_colour, x - 1, y, original_colour, non_matching) +
+ fill_board(board, fill_colour, x, y - 1, original_colour, non_matching) +
+ fill_board(board, fill_colour, x + 1, y, original_colour, non_matching) +
+ fill_board(board, fill_colour, x, y + 1, original_colour, non_matching) + 1
+ elseif(non_matching ~= nil and board[x][y] ~= fill_colour) then
+ table.insert(non_matching, {x,y})
+ end
+ end
+
+ return 0
+end
+
+--Checks whether the given board is a single colour
+function check_win(board)
+ for x,col in pairs(board) do
+ for y,value in pairs(col) do
+ if value ~= board[1][1] then
+ return false
+ end
+ end
+ end
+
+ return true
+end
+
+--Attempt to load the game variables stored in the indicated save file.
+--Returns a table containing game variables if the file can be opened,
+--false otherwise.
+--Table keys are: difficulty, par, move_number, selected_colour, board
+function load_game(filename)
+ local f = io.open(filename, "r")
+ if f ~= nil then
+ local rtn = {}
+ rtn["difficulty"] = tonumber(f:read())
+ rtn["par"] = tonumber(f:read())
+ rtn["move_number"] = tonumber(f:read())
+ rtn["selected_colour"] = tonumber(f:read())
+
+ local board={}
+ local dimension = diff_to_dimension(rtn["difficulty"])
+ for x=1,dimension do
+ board[x] = {}
+ local line = f:read()
+ local bits = {line:match(("([^ ]*) "):rep(dimension))}
+ for y=1,dimension do
+ board[x][y] = tonumber(bits[y])
+ end
+ end
+ rtn["board"] = board
+
+ f:close()
+ return rtn
+ else
+ return false
+ end
+end
+
+--Saves the game state to file
+function save_game(state, filename)
+ local f = io.open(filename, "w")
+ if f ~= nil then
+ f:write(state["difficulty"],"\n")
+ f:write(state["par"],"\n")
+ f:write(state["move_number"],"\n")
+ f:write(state["selected_colour"],"\n")
+ local board = state["board"]
+ local dimension = diff_to_dimension(state["difficulty"])
+ for x=1,dimension do
+ for y=1,dimension do
+ f:write(board[x][y]," ")
+ end
+ f:write("\n")
+ end
+ f:close()
+ return true
+ else
+ return false
+ end
+end
+
+--Loads the high scores from file
+--Returns true on success, false otherwise
+function load_scores(filename)
+ local f = io.open(filename, "r")
+ if f ~= nil then
+ local highscores = {}
+ for i=1,3 do
+ local line = f:read()
+ local value = false
+ if line ~= nil then
+ value = tonumber(line)
+ end
+
+ highscores[i] = value
+ end
+ f:close()
+ return highscores
+ else
+ return false
+ end
+end
+
+--Saves the high scores to file
+function save_scores(highscores, filename)
+ local f = io.open(filename, "w")
+ if f ~= nil then
+ for i=1,3 do
+ local value = highscores[i]
+ if value == false then
+ value = ""
+ end
+ f:write(value,"\n")
+ end
+ f:close()
+ return true
+ else
+ return false
+ end
+end
+
+function diff_to_dimension(difficulty)
+ if difficulty == 1 then
+ return 8
+ elseif difficulty == 2 then
+ return 16
+ else
+ return 22
+ end
+end
+
+--Don't run the RB stuff if we're not running under RB
+if rb ~= nil then
+
+ if rb.lcd_rgbpack == nil then
+ rb.splash(2*rb.HZ, "Non RGB targets not currently supported")
+ os.exit()
+ end
+
+ ---------------------
+ --RB Game variables--
+ ---------------------
+
+ --The colours used by the game
+ COLOURS = {
+ rb.lcd_rgbpack(255, 119, 34),
+ rb.lcd_rgbpack(255, 255, 102),
+ rb.lcd_rgbpack(119, 204, 51),
+ rb.lcd_rgbpack(102, 170, 255),
+ rb.lcd_rgbpack(51, 68, 255),
+ rb.lcd_rgbpack(51, 51, 51),
+ }
+ --The colour of the selection pip
+ PIP_COLOURS = {
+ rb.lcd_rgbpack(0, 0, 0),
+ rb.lcd_rgbpack(0, 0, 0),
+ rb.lcd_rgbpack(0, 0, 0),
+ rb.lcd_rgbpack(0, 0, 0),
+ rb.lcd_rgbpack(0, 0, 0),
+ rb.lcd_rgbpack(255, 255, 255),
+ }
+ DEFAULT_DIFFICULTY = 2 --1: Easy, 2: Normal, 3: Hard
+
+ FILES_ROOT = "/.rockbox/rocks/games/"
+ SCORES_FILE = FILES_ROOT.."pixel-painter.score"
+ SAVE_FILE = FILES_ROOT.."pixel-painter.save"
+ r,w,TEXT_LINE_HEIGHT = rb.font_getstringsize(" ", rb.FONT_UI) --Get font height
+ --Determine which layout to use by considering the screen dimensions
+ --the +9 is so we have space for the chooser
+ if rb.LCD_WIDTH > (rb.LCD_HEIGHT + 9) then
+ LAYOUT = 1 --Wider than high, status and chooser on right
+ elseif rb.LCD_HEIGHT > (rb.LCD_WIDTH + 9) then
+ LAYOUT = 2 --Higher than wide, status and chooser below
+ else
+ LAYOUT = 3 --Treat like a square screen, chooser on right, status below
+ end
+
+ --Display variables
+ chooser_pip_dimension = 6 --pixel dimension of the selected colour pip
+ block_width = 0 --pixel dimension of each game square
+ chooser_start_pos = 0 --x or y position of the first block (depending on LAYOUT)
+
+ --Populated by load_scores()
+ highscores = {false, false, false}
+
+ --A table containing the game state, initialised by init_game() or
+ --load_game(), see
+ game_state = {}
+
+ -----------------------------------
+ --Display and interface functions--
+ -----------------------------------
+
+ --Sets up a new game and display variables for the indicated
+ --difficulty
+ function init_game(difficulty)
+ init_display_variables(difficulty)
+ local state = {}
+ local board_dimension = diff_to_dimension(difficulty)
+ state["selected_colour"] = 1
+ state["move_number"] = 0
+ state["difficulty"] = difficulty
+ state["board"] = generate_board(board_dimension, rb.current_tick()+os.time())
+ rb.splash(1, "Calculating par...") --Will stay on screen until it's done
+ state["par"] = calculate_par(state["board"])
+
+ return state
+ end
+
+ --Initialises the display variables for the screen LAYOUT
+ function init_display_variables(difficulty)
+ local board_dimension = diff_to_dimension(difficulty)
+
+ if LAYOUT == 1 then
+ block_width = rb.LCD_HEIGHT / board_dimension
+ chooser_start_pos = (board_dimension)*block_width + 2
+ chooser_width = rb.LCD_WIDTH - chooser_start_pos
+ chooser_height = (rb.LCD_HEIGHT - 3*TEXT_LINE_HEIGHT) / NUM_COLOURS
+ elseif LAYOUT == 2 then
+ block_width = rb.LCD_WIDTH / board_dimension
+ chooser_start_pos = board_dimension*block_width + 2 + TEXT_LINE_HEIGHT
+ chooser_width = rb.LCD_WIDTH / NUM_COLOURS
+ chooser_height = rb.LCD_HEIGHT - chooser_start_pos
+ else
+ if TEXT_LINE_HEIGHT > 9 then
+ block_width = (rb.LCD_HEIGHT - TEXT_LINE_HEIGHT) / board_dimension
+ else
+ block_width = (rb.LCD_HEIGHT - 9) / board_dimension
+ end
+ chooser_start_pos = (board_dimension)*block_width + 1
+ chooser_width = rb.LCD_WIDTH - chooser_start_pos
+ chooser_height = (rb.LCD_HEIGHT - TEXT_LINE_HEIGHT) / NUM_COLOURS
+ end
+ end
+
+ --Draws the game board to screen
+ function draw_board(board)
+ for x,col in pairs(board) do
+ for y,value in pairs(col) do
+ rb.lcd_set_foreground(COLOURS[value])
+ rb.lcd_fillrect((x-1)*block_width, (y-1)*block_width, block_width, block_width)
+ end
+ end
+ end
+
+ --Draw the colour chooser along with selected pip at the appropriate
+ --position
+ function draw_chooser(selected_colour)
+ for i=1,NUM_COLOURS do
+ rb.lcd_set_foreground(COLOURS[i])
+ if LAYOUT == 1 or LAYOUT == 3 then
+ rb.lcd_fillrect(chooser_start_pos, (i - 1)*(chooser_height), chooser_width, chooser_height)
+ elseif LAYOUT == 2 then
+ rb.lcd_fillrect((i - 1)*(chooser_width), chooser_start_pos, chooser_width, chooser_height)
+ end
+ end
+
+ rb.lcd_set_foreground(PIP_COLOURS[selected_colour])
+ local xpos = 0
+ local ypos = 0
+ if LAYOUT == 1 or LAYOUT == 3 then
+ xpos = chooser_start_pos + (chooser_width - chooser_pip_dimension)/2
+ ypos = (selected_colour-1)*(chooser_height) + (chooser_height - chooser_pip_dimension)/2
+ elseif LAYOUT == 2 then
+ xpos = (selected_colour-1)*(chooser_width) + (chooser_width - chooser_pip_dimension)/2
+ ypos = chooser_start_pos + (chooser_height - chooser_pip_dimension)/2
+ end
+ rb.lcd_fillrect(xpos, ypos, chooser_pip_dimension, chooser_pip_dimension)
+ end
+
+ --Draws the current moves, par and high score
+ function draw_status(move_number, par, highscore)
+ local strings = {"Move", move_number, "Par", par}
+ if highscore then
+ table.insert(strings, "Best")
+ table.insert(strings, highscore)
+ end
+
+ if LAYOUT == 1 then
+ local function calc_string(var_len_str, static_str)
+ local avail_width = chooser_width - rb.font_getstringsize(static_str, rb.FONT_UI)
+ local rtn_str = ""
+
+ for i=1,string.len(var_len_str) do
+ local c = string.sub(var_len_str, i, i)
+ local curr_width = rb.font_getstringsize(rtn_str, rb.FONT_UI)
+ local width = rb.font_getstringsize(c, rb.FONT_UI)
+
+ if curr_width + width <= avail_width then
+ rtn_str = rtn_str .. c
+ else
+ break
+ end
+ end
+
+ return rtn_str, rb.font_getstringsize(rtn_str, rb.FONT_UI)
+ end
+
+ local height = NUM_COLOURS*chooser_height
+ colon_width = rb.font_getstringsize(": ", rb.FONT_UI)
+ for i = 1,table.getn(strings),2 do
+ local label, label_width = calc_string(strings[i], ": "..strings[i+1])
+
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0))
+ rb.lcd_putsxy(chooser_start_pos, height, label..": ")
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255))
+ rb.lcd_putsxy(chooser_start_pos + label_width + colon_width, height, strings[i+1])
+ height = height + TEXT_LINE_HEIGHT
+ end
+ else
+ local text_ypos = 0
+ if LAYOUT == 2 then
+ text_ypos = chooser_start_pos - TEXT_LINE_HEIGHT - 1
+ else
+ text_ypos = rb.LCD_HEIGHT - TEXT_LINE_HEIGHT
+ end
+ space_width = rb.font_getstringsize(" ", rb.FONT_UI)
+ local xpos = 0
+ for i = 1,table.getn(strings),2 do
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0))
+ rb.lcd_putsxy(xpos, text_ypos, strings[i]..": ")
+ xpos = xpos + rb.font_getstringsize(strings[i]..": ", rb.FONT_UI)
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255))
+ rb.lcd_putsxy(xpos, text_ypos, strings[i+1])
+ xpos = xpos + rb.font_getstringsize(strings[i+1], rb.FONT_UI) + space_width
+ end
+ end
+ end
+
+ --Convenience function to redraw the whole board to screen
+ function redraw_game(game_state, highscores)
+ rb.lcd_clear_display()
+ draw_board(game_state["board"])
+ draw_chooser(game_state["selected_colour"])
+ draw_status(game_state["move_number"], game_state["par"],
+ highscores[game_state["difficulty"]])
+ rb.lcd_update()
+ end
+
+
+ --Draws help to screen, waits for a keypress to exit
+ function app_help()
+ rb.lcd_clear_display()
+
+ local title = "Pixel painter help"
+ local rtn, title_width, h = rb.font_getstringsize(title, rb.FONT_UI)
+ local title_xpos = (rb.LCD_WIDTH - title_width) / 2
+ local space_width = rb.font_getstringsize(" ", rb.FONT_UI)
+
+ --Draw title
+ function draw_text(y_offset)
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,0,0))
+ rb.lcd_putsxy(title_xpos, y_offset, title)
+ rb.lcd_hline(title_xpos, title_xpos + title_width, TEXT_LINE_HEIGHT + y_offset)
+ rb.lcd_set_foreground(rb.lcd_rgbpack(255,255,255))
+
+ local body_text = [[
+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.
+
+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.
+ ]]
+ local body_len = string.len(body_text)
+
+ --Draw body text
+ local word_buffer = ""
+ local xpos = 0
+ local ypos = TEXT_LINE_HEIGHT * 2
+ for i=1,body_len do
+ local c = string.sub(body_text, i, i)
+ if c == " " or c == "\n" then
+ local word_length = rb.font_getstringsize(word_buffer, rb.FONT_UI)
+ if (xpos + word_length) > rb.LCD_WIDTH then
+ xpos = 0
+ ypos = ypos + TEXT_LINE_HEIGHT
+ end
+ rb.lcd_putsxy(xpos, ypos + y_offset, word_buffer)
+
+ word_buffer = ""
+ if c == "\n" then
+ xpos = 0
+ ypos = ypos + TEXT_LINE_HEIGHT
+ else
+ xpos = xpos + word_length + space_width
+ end
+ else
+ word_buffer = word_buffer .. c
+ end
+ end
+
+ rb.lcd_update()
+
+ return ypos
+ end
+
+ --Deal with scrolling the help
+ local y_offset = 0
+ local max_y_offset = math.max(draw_text(y_offset) - rb.LCD_HEIGHT, 0)
+ local exit = false
+ repeat
+ local action = rb.get_plugin_action(0)
+ if action == rb.actions.PLA_DOWN then
+ y_offset = math.max(-max_y_offset, y_offset - TEXT_LINE_HEIGHT)
+ elseif action == rb.actions.PLA_UP then
+ y_offset = math.min(0, y_offset + TEXT_LINE_HEIGHT)
+ elseif action == rb.actions.PLA_LEFT or
+ action == rb.actions.PLA_RIGHT or
+ action == rb.actions.PLA_SELECT or
+ action == rb.actions.PLA_EXIT or
+ action == rb.actions.PLA_CANCEL then
+ --This explicit enumeration is needed for targets like
+ --the iriver which send more than one action when
+ --scrolling
+
+ exit = true
+ end
+ rb.lcd_clear_display()
+ draw_text(y_offset)
+ until exit == true
+ end
+
+ --Draws the application menu and handles its logic
+ function app_menu()
+ local options = {"Resume game", "Start new game", "Change difficulty",
+ "Help", "Quit without saving", "Quit"}
+ local item = rb.do_menu("Pixel painter menu", options, nil, false)
+
+ if item == 0 then
+ redraw_game(game_state, highscores)
+ elseif item == 1 then
+ os.remove(SAVE_FILE)
+ game_state = init_game(game_state["difficulty"])
+ redraw_game(game_state, highscores)
+ elseif item == 2 then
+ local diff = rb.do_menu("Difficulty", {"Easy", "Medium", "Hard"}, game_state["difficulty"] - 1, false)
+ if diff < 0 then
+ app_menu()
+ else
+ local difficulty = diff + 1 --lua is 1 indexed
+ os.remove(SAVE_FILE)
+ game_state = init_game(difficulty)
+ redraw_game(game_state, highscores)
+ end
+ elseif item == 3 then
+ app_help()
+ redraw_game(game_state, highscores)
+ elseif item == 4 then
+ os.remove(SAVE_FILE)
+ os.exit()
+ elseif item == 5 then
+ rb.splash(1, "Saving game...") --Will stay on screen till the app exits
+ save_game(game_state,SAVE_FILE)
+ os.exit()
+ end
+ end
+
+ --Determine what victory text to show depending on the relation of the
+ --score to the calculated par value
+ function win_text(delta)
+ if delta < 0 then
+ return "You were "..(-1*delta).." under par"
+ elseif delta > 0 then
+ return "You were "..delta.." over par"
+ else
+ return "You attained par"
+ end
+ end
+
+ ------------------
+ --Game main loop--
+ ------------------
+
+ --Gives the option of testing things without running the game, use:
+ --as_library=true
+ --dofile('pixel-painter.lua')
+ if not as_library then
+ game_state = load_game(SAVE_FILE)
+ if game_state then
+ init_display_variables(game_state["difficulty"])
+ else
+ game_state = init_game(DEFAULT_DIFFICULTY)
+ end
+ loaded_scores = load_scores(SCORES_FILE)
+ if loaded_scores then
+ highscores = loaded_scores
+ end
+ redraw_game(game_state, highscores)
+
+ require("actions")
+ --Set the keys to use for scrolling the chooser
+ if LAYOUT == 1 or LAYOUT == 3 then -- landscape and square screens
+ prev_action = rb.actions.PLA_UP
+ prev_action_repeat = rb.actions.PLA_UP_REPEAT
+ next_action = rb.actions.PLA_DOWN
+ next_action_repeat = rb.actions.PLA_DOWN_REPEAT
+ else -- portrait screens
+ prev_action = rb.actions.PLA_LEFT
+ prev_action_repeat = rb.actions.PLA_LEFT_REPEAT
+ next_action = rb.actions.PLA_RIGHT
+ next_action_repeat = rb.actions.PLA_RIGHT_REPEAT
+ end
+
+ repeat
+ local action = rb.get_plugin_action(-1) -- TIMEOUT_BLOCK
+
+ if action == rb.actions.PLA_SELECT then
+ --Ensure the user has changed the colour before allowing move
+ --TODO: Check that the move would change the board
+
+ if game_state["selected_colour"] ~= game_state["board"][1][1] then
+ fill_board(game_state["board"], game_state["selected_colour"],
+ 1, 1, game_state["board"][1][1])
+ game_state["move_number"] = game_state["move_number"] + 1
+ redraw_game(game_state, highscores)
+
+ if check_win(game_state["board"]) then
+ local par_diff = game_state["move_number"] - game_state["par"]
+ if not highscores[game_state["difficulty"]] or
+ par_diff < highscores[game_state["difficulty"]] then
+ --
+ rb.splash(3*rb.HZ, win_text(par_diff)..", a new high score!")
+ highscores[game_state["difficulty"]] = par_diff
+ save_scores(highscores, SCORES_FILE)
+ else
+ rb.splash(3*rb.HZ, win_text(par_diff)..".")
+ end
+ os.remove(SAVE_FILE)
+ os.exit()
+ end
+ else
+ --Will stay on screen until they move
+ rb.splash(1, "Invalid move (wouldn't change board). Change colour to continue.")
+ end
+ elseif action == next_action or action == next_action_repeat then
+ if game_state["selected_colour"] < NUM_COLOURS then
+ game_state["selected_colour"] = game_state["selected_colour"] + 1
+ else
+ game_state["selected_colour"] = 1
+ end
+ redraw_game(game_state, highscores)
+ elseif action == prev_action or action == prev_action_repeat then
+ if game_state["selected_colour"] > 1 then
+ game_state["selected_colour"] = game_state["selected_colour"] - 1
+ else
+ game_state["selected_colour"] = NUM_COLOURS
+ end
+ redraw_game(game_state, highscores)
+ elseif action == rb.actions.PLA_CANCEL then
+ app_menu()
+ end
+ until action == rb.actions.PLA_EXIT
+ --This is executed if the user presses PLA_EXIT
+ rb.splash(1, "Saving game...") --Will stay on screen till the app exits
+ save_game(game_state,SAVE_FILE)
+ end
+end
diff --git a/docs/CREDITS b/docs/CREDITS
index 81a93d651f..b14847b129 100644
--- a/docs/CREDITS
+++ b/docs/CREDITS
@@ -657,6 +657,7 @@ Adam Sampson
William Wilgus
Igor Skochinsky
Sebastiano Pistore
+Stefan Schneider-Kennedy
The libmad team
The wavpack team
diff --git a/manual/plugins/images/ss-pixelpainter-128x128x16.png b/manual/plugins/images/ss-pixelpainter-128x128x16.png
new file mode 100644
index 0000000000..927880a00c
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-128x128x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-128x160x16.png b/manual/plugins/images/ss-pixelpainter-128x160x16.png
new file mode 100644
index 0000000000..125c6f652b
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-128x160x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-128x96x16.png b/manual/plugins/images/ss-pixelpainter-128x96x16.png
new file mode 100644
index 0000000000..84819dd52c
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-128x96x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-132x80x16.png b/manual/plugins/images/ss-pixelpainter-132x80x16.png
new file mode 100644
index 0000000000..a1e0830cb5
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-132x80x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-160x128x16.png b/manual/plugins/images/ss-pixelpainter-160x128x16.png
new file mode 100644
index 0000000000..f5c20ef95d
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-160x128x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-176x132x16.png b/manual/plugins/images/ss-pixelpainter-176x132x16.png
new file mode 100644
index 0000000000..06cdaf6d9d
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-176x132x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-176x220x16.png b/manual/plugins/images/ss-pixelpainter-176x220x16.png
new file mode 100644
index 0000000000..c41a3f6f00
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-176x220x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-220x176x16.png b/manual/plugins/images/ss-pixelpainter-220x176x16.png
new file mode 100644
index 0000000000..a92eefeba8
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-220x176x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-240x320x16.png b/manual/plugins/images/ss-pixelpainter-240x320x16.png
new file mode 100644
index 0000000000..ceb7c4f7a6
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-240x320x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-240x400x16.png b/manual/plugins/images/ss-pixelpainter-240x400x16.png
new file mode 100644
index 0000000000..08c467c131
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-240x400x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-320x240x16.png b/manual/plugins/images/ss-pixelpainter-320x240x16.png
new file mode 100644
index 0000000000..82d1d68984
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-320x240x16.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-320x240x24.png b/manual/plugins/images/ss-pixelpainter-320x240x24.png
new file mode 100644
index 0000000000..82d1d68984
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-320x240x24.png
Binary files differ
diff --git a/manual/plugins/images/ss-pixelpainter-96x96x16.png b/manual/plugins/images/ss-pixelpainter-96x96x16.png
new file mode 100644
index 0000000000..eb0f5bf668
--- /dev/null
+++ b/manual/plugins/images/ss-pixelpainter-96x96x16.png
Binary files differ
diff --git a/manual/plugins/main.tex b/manual/plugins/main.tex
index 540a0efe5c..bf5bcd3dac 100644
--- a/manual/plugins/main.tex
+++ b/manual/plugins/main.tex
@@ -70,6 +70,8 @@ text files%
\opt{lcd_bitmap}{\input{plugins/pegbox.tex}}
+\opt{lcd_color}{\opt{large_plugin_buffer}{\input{plugins/pixelpainter.tex}}}
+
\opt{lcd_bitmap}{\input{plugins/pong.tex}}
\opt{lcd_bitmap}{\input{plugins/reversi.tex}}
diff --git a/manual/plugins/pixelpainter.tex b/manual/plugins/pixelpainter.tex
new file mode 100644
index 0000000000..2b6a315480
--- /dev/null
+++ b/manual/plugins/pixelpainter.tex
@@ -0,0 +1,24 @@
+\subsection{Pixel Painter}
+\screenshot{plugins/images/ss-pixelpainter}{Pixel Painter}{img:pixelpainter}
+This game is written in LUA and based on the game of the same name by
+Pavel Bakhilau (\url{http://js1k.com/2010-first/demo/453}).
+
+Select a colour to flood-fill the board with that colour, starting from the
+top-left pixel (meaning that any pixel which is connected to the top-left
+through other pixels of the same colour will be changed to the selected colour).
+Try to paint the entire board with as few moves as possible.
+
+\begin{btnmap}
+ \ifnum\dapdisplaywidth<\dapdisplayheight
+ \PluginLeft{} / \PluginRight
+ \else
+ \PluginUp{} / \PluginDown
+ \fi
+ & Move colour selector\\
+
+ \PluginSelect
+ & Fill screen with selected colour\\
+
+ \PluginCancel, \PluginExit
+ & Enter game menu\\
+\end{btnmap}