diff --git a/.info.pod b/.info.pod new file mode 100644 index 0000000..c9fcc9a Binary files /dev/null and b/.info.pod differ diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..32458d5 --- /dev/null +++ b/Readme.md @@ -0,0 +1,3 @@ +# Puzzle Fighter + +![](captures/alpha_1.gif) \ No newline at end of file diff --git a/captures/alpha_1.gif b/captures/alpha_1.gif new file mode 100644 index 0000000..a2cdd6d Binary files /dev/null and b/captures/alpha_1.gif differ diff --git a/contributing.txt b/contributing.txt new file mode 100644 index 0000000..9692459 --- /dev/null +++ b/contributing.txt @@ -0,0 +1,66 @@ +--[[pod_format="raw",created="2024-05-24 11:33:38",modified="2024-05-24 11:52:39",revision=11]] +-==================================================- + CODE STANDARDS +-==================================================- + +** Naming things ** + +VARIABLE is a constant (must not be edited) +_variable is a global variable +variable is a standard variable (must be "local") + +** Adding things ** + +/main.lua is the unmovable entry point +/src is for all the code + /state is for state files + /character is for characters files + + +-==================================================- + PROGRAMMING PATTERNS +-==================================================- + +** States ** + +All the code must be encapsulated in "states". + +Each "state" is nearly a screen in the game: + + - Main menu + - Options + - Character selection + - In game + - ... + +All the necessary variables to make the state running +have to be declared in the state: + +my_state = { + foo = "bar", -- Foo is used in this screen +} + +All the states must have init, draw and update +functions: + +function my_state:init(options) + -- Things to do when we need to initialize the + -- state, using options. +end + +function my_state:draw() + -- The code we want to run in the Picotron _draw + -- function. +end + +function my_state:update() + -- The code we want to run in the Picotron _update + -- function. +end + +To change the current state, simply assign the new +state to the "_state" global variable. + +Keep in mind that changing the state will pick up +where it left off until you manually call the "init" +function. \ No newline at end of file diff --git a/gfx/.info.pod b/gfx/.info.pod new file mode 100644 index 0000000..36527a7 Binary files /dev/null and b/gfx/.info.pod differ diff --git a/gfx/0.gfx b/gfx/0.gfx new file mode 100644 index 0000000..e076dfd Binary files /dev/null and b/gfx/0.gfx differ diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..0f172ca --- /dev/null +++ b/main.lua @@ -0,0 +1,30 @@ +--[[pod_format="raw",created="2024-05-23 17:40:17",modified="2024-05-25 20:32:35",revision=1935]] +--[[ + + Pico Puzzle Fighter + + by P-Rex for Pixelocene + +]] + +include "src/debug.lua" +include "src/data_tree.lua" +include "src/conf.lua" +include "src/state/main_menu.lua" +include "src/state/game.lua" + +_state = nil + +function _init() + game_state:init() + _state = game_state +end + +function _draw() + _state:draw() + _debug:draw() +end + +function _update() + _state:update() +end \ No newline at end of file diff --git a/map/.info.pod b/map/.info.pod new file mode 100644 index 0000000..5203c15 Binary files /dev/null and b/map/.info.pod differ diff --git a/map/0.map b/map/0.map new file mode 100644 index 0000000..bd6967f Binary files /dev/null and b/map/0.map differ diff --git a/sfx/.info.pod b/sfx/.info.pod new file mode 100644 index 0000000..7be5c95 Binary files /dev/null and b/sfx/.info.pod differ diff --git a/sfx/0.sfx b/sfx/0.sfx new file mode 100644 index 0000000..cf70573 Binary files /dev/null and b/sfx/0.sfx differ diff --git a/src/.info.pod b/src/.info.pod new file mode 100644 index 0000000..2ad1c83 Binary files /dev/null and b/src/.info.pod differ diff --git a/src/board.lua b/src/board.lua new file mode 100644 index 0000000..7352076 --- /dev/null +++ b/src/board.lua @@ -0,0 +1,293 @@ +--[[pod_format="raw",created="2024-05-23 17:41:23",modified="2024-05-25 20:32:47",revision=2272]] +--[[ + + Board management + +]] + +Board = {} + +function Board:new(side, width, height) + + side = side or 0 + board_width = width or BOARD_WIDTH + board_height = height or BOARD_HEIGHT + drop_start_position = ceil(board_width / 2) + grid_width = GEM_SIZE * board_width + grid_height = GEM_SIZE * board_height + + left_margin = (SCREEN_WIDTH - grid_width) / 2 + top_margin = (SCREEN_HEIGHT - grid_height) / 2 + + if side == 1 then + left_margin = BOARD_X_MARGIN + elseif side == 2 then + left_margin = SCREEN_WIDTH - BOARD_X_MARGIN - BOARD_WIDTH * GEM_SIZE + end + + -- Build the board + + local grid = {} + for i=1, board_height do + local grid_line = {} + for i=1, board_width do + add(grid_line, 0) + end + add(grid, grid_line) + end + + o = { + drop_counter = 0, -- The number of drops (get a rainbow gem each 25 drops) + drop = nil, + next_drop = nil, + drop_speed = 0, -- Modification of the BASE SPEED (increase while playing) + grid = grid, + state = BOARD_STATE_RUNNING, + side = side, + board_width = board_width, + board_height = board_height, + drop_start_position = drop_start_position, + grid_width = grid_width, + grid_height = grid_height, + left_margin = left_margin, + top_margin = top_margin, + explosion_counter = 0, + chain_counter = 0, + } + + setmetatable(o, self) + self.__index = self + + return o +end + +function Board:draw(side) + + -- Draw the board and next drop background + + --poke(0x550b, 0x3f) + --palt() + fillp(0xa5a5) -- 1x1px grid + + rectfill( + left_margin, + top_margin, + left_margin + grid_width - 1, -- There is an extra pixel we want to remove + top_margin + grid_height - 1, + 1 + ) + + rectfill( + left_margin + grid_width + 10, + top_margin, + left_margin + grid_width + 26 - 1, + top_margin + 32 - 1, + 1 + ) + + fillp() -- Reset the fill pattern (applied on sprites too) + + -- Draw the next drop + + self.next_drop.secondary:draw(left_margin + grid_width + 10, top_margin) + self.next_drop.main:draw(left_margin + grid_width + 10, top_margin + 16) + + -- Draw the gems + + for i, lin in pairs(self.grid) do + for j, col in pairs(lin) do + local gem = Gem:new(RED_GEM) + if self.grid[i][j] != 0 then + self.grid[i][j]:draw(self:coords_to_pixels(j, i)) + end + end + end + + -- Draw the drop + + self.drop.main:draw(self:coords_to_pixels(self.drop.x, self.drop.y)) + + -- Draw the main gem outline + + if self.drop.main.type < 5 then + spr(5, self:coords_to_pixels(self.drop.x, self.drop.y)) + elseif self.drop.main.type != RAINBOW_CRASHER then + spr(13, self:coords_to_pixels(self.drop.x, self.drop.y)) + end + + -- Draw the secondary gem + + self.drop.secondary:draw(self:coords_to_pixels(self.drop:secondary_coords())) + +end + +--- Check if the given position is valid or not. +-- It checks if we are out of boundaries or if the grid is full at this position. +function Board:position_is_valid(x, y) + + if y > self.board_height then return false end + if x > self.board_width then return false end + if x < 1 then return false end + + return self.grid[y][x] == 0 + +end + +--- Everything must no escape gravity. +function Board:apply_physics() + if self.state == BOARD_STATE_RUNNING then + self.chain_counter = 0 + self.drop:fall() + end + if self.state == BOARD_STATE_AUTO_FALL then + self:auto_fall() + end + if self.state == BOARD_STATE_EXPLODING then + self:auto_explode() + end +end + +--- Store the drop in the grid and require a new Drop +function Board:persist_drop() + if self.drop.grace and self.drop.grace_delay <= 0 then + local x, y = self.drop:secondary_coords() + + local main_x = flr(self.drop.x) + local main_y = ceil(self.drop.y) + local secondary_x = flr(x) + local secondary_y = ceil(y) + + self:set_gem(main_x, main_y, self.drop.main) + self:set_gem(secondary_x, secondary_y, self.drop.secondary) + + self.state = BOARD_STATE_AUTO_FALL + self:new_drop() + end +end + +--- Get a new drop +-- The next_drop is moved to drop if existing. +-- If next_drop is nil, we create a new Drop instead. +-- It also ensure that next_drop has a new Drop inside. +function Board:new_drop() + self.drop_counter += 1 + + if self.next_drop == nil then self.next_drop = Drop:new(self) end + + self.drop = self.next_drop + self.next_drop = Drop:new(self) +end + +--- Set the Gem to the given position. +function Board:set_gem(x, y, gem) + self.grid[y][x] = gem +end + +--- Convert a "grid position" to a "screen position" by applying margins. +function Board:coords_to_pixels(x, y) + local x = self.left_margin + (x - 1) * GEM_SIZE + local y = self.top_margin + (y - 1) * GEM_SIZE + + return x, y +end + +--- Automatically make gems with empty spaces under falling +function Board:auto_fall() + local falling = false + + for i=#self.grid-1,1,-1 do + for j,gem in ipairs(self.grid[i]) do + if self.grid[i][j] != 0 and self.grid[i+1][j] == 0 then + self.grid[i+1][j] = gem + self.grid[i][j] = 0 + falling = true + end + end + end + + if not falling then self.state = BOARD_STATE_EXPLODING end +end + +function Board:auto_explode() + -- Increment the chain counter to increase points + self.explosion_counter = 0 + self.chain_counter += 1 + + -- Look for all crashers and recursively look at neighboors to mark them for explosion + for i,lin in ipairs(self.grid) do + for j,gem in ipairs(self.grid[i]) do + if gem != 0 then + if gem.type >= 5 and gem.type <= 8 then + -- It's a crasher + self:mark_gems_for_explosion(j, i, gem) + end + end + end + end + + -- Explode all the marked gems + for i,lin in ipairs(self.grid) do + for j,gem in ipairs(self.grid[i]) do + if gem != 0 then + if gem.exploding then + self:set_gem(j, i, 0) + end + end + end + end + + if self.explosion_counter == 0 then + self.state = BOARD_STATE_RUNNING + else + self.state = BOARD_STATE_AUTO_FALL + end + +end + +function Board:mark_gems_for_explosion(x, y, gem) + + -- Upper gem + if y > 1 then + local top = self.grid[y - 1][x] + if top != 0 and not top.exploding and gem:color_match(top.type) then + gem.exploding = true + top.exploding = true + self.explosion_counter += 1 + self:mark_gems_for_explosion(x, y - 1, top) + end + end + + -- Lower gem + if y < #self.grid then + local bottom = self.grid[y + 1][x] + if bottom != 0 and not bottom.exploding and gem:color_match(bottom.type) then + gem.exploding = true + bottom.exploding = true + self.explosion_counter += 1 + self:mark_gems_for_explosion(x, y + 1, bottom) + end + end + + -- Left gem + if x > 1 then + local left = self.grid[y][x - 1] + if left != 0 and not left.exploding and gem:color_match(left.type) then + gem.exploding = true + left.exploding = true + self.explosion_counter += 1 + self:mark_gems_for_explosion(x - 1, y, left) + end + end + + -- Right gem + if x < #self.grid[1] then + local right = self.grid[y][x + 1] + if right != 0 and not right.exploding and gem:color_match(right.type) then + gem.exploding = true + right.exploding = true + self.explosion_counter += 1 + self:mark_gems_for_explosion(x + 1, y, right) + end + end + +end \ No newline at end of file diff --git a/src/conf.lua b/src/conf.lua new file mode 100644 index 0000000..a310968 --- /dev/null +++ b/src/conf.lua @@ -0,0 +1,79 @@ +--[[pod_format="raw",created="2024-05-24 08:18:32",modified="2024-05-25 20:32:35",revision=1261]] +--[[ + + Configuration variables + +]] + +-- System informations + +SCREEN_WIDTH = 480 +SCREEN_HEIGHT = 270 + +DEBUG = true + +-- Game modes + +MODE_SOLO_ENDLESS = 1 +MODE_P_VS_AI = 2 +MODE_P_VS_P = 3 + +-- Buttons codes + +BTN_P1_LEFT = 0 +BTN_P1_RIGHT = 1 +BTN_P1_UP = 2 +BTN_P1_DOWN = 3 +BTN_P1_LROT = 4 +BTN_P1_RROT = 5 + +BTN_P2_LEFT = 9 +BTN_P2_RIGHT = 10 +BTN_P2_UP = 11 +BTN_P2_DOWN = 12 +BTN_P2_LROT = 13 +BTN_P2_RROT = 14 + +-- Board rendering tweeks + +GEM_SIZE = 16 + +BOARD_X_MARGIN = 20 +BOARD_WIDTH = 6 +BOARD_HEIGHT = 13 + +-- Gems + +GREEN_GEM = 1 +BLUE_GEM = 2 +RED_GEM = 3 +YELLOW_GEM = 4 +GREEN_CRASHER = 5 +BLUE_CRASHER = 6 +RED_CRASHER = 7 +YELLOW_CRASHER = 8 +RAINBOW_CRASHER = 9 + +-- Game configuration + +GRACE_DELAY = 3 +BASE_SPEED = 0.01 +FAST_SPEED = 0.4 + +-- Secondary drop position + +TOP = 1 +RIGHT = 2 +BOTTOM = 3 +LEFT = 4 + +-- Board side + +LEFT_SIDE = 1 +RIGHT_SIDE = 2 + +-- GAME_STATE internal states + +BOARD_STATE_RUNNING = 1 -- Standard state, the player can control the drop +BOARD_STATE_AUTO_FALL = 2 -- Make gems with empty space under falling +BOARD_STATE_EXPLODING = 3 -- Frozen state, can't move until animations ended \ No newline at end of file diff --git a/src/data_tree.lua b/src/data_tree.lua new file mode 100644 index 0000000..81c33c2 --- /dev/null +++ b/src/data_tree.lua @@ -0,0 +1,10 @@ +--[[pod_format="raw",created="2024-05-25 13:14:53",modified="2024-05-25 13:41:12",revision=118]] +--[[ + + Data Tree + +]] + +_data_tree = { + state = nil, +} \ No newline at end of file diff --git a/src/debug.lua b/src/debug.lua new file mode 100644 index 0000000..8620aa0 --- /dev/null +++ b/src/debug.lua @@ -0,0 +1,37 @@ +--[[pod_format="raw",created="2024-05-25 14:23:26",modified="2024-05-25 18:38:51",revision=549]] +--[[ + + Debug utilities + +]] + +_debug = { + vars = {}, +} + +function _debug:push(var) + if var == nil then var = "nil" end + if var == true then var = "true" end + if var == false then var = "false" end + + var = ""..var -- force string + + add(self.vars, var) + + if #self.vars > 10 then deli(self.vars, 1) end +end + +function _debug:draw() + local char_width = 4 + local char_height = 9 + + if not DEBUG then return end + + for i, entry in ipairs(self.vars) do + local x = SCREEN_WIDTH - (#entry + 1) * char_width + local y = (i - 1) * char_height + + rectfill(x, y, x + (#entry + 1) * char_width, y + char_height, 0) + print(entry, x, y, 7) + end +end \ No newline at end of file diff --git a/src/drop.lua b/src/drop.lua new file mode 100644 index 0000000..722e9c2 --- /dev/null +++ b/src/drop.lua @@ -0,0 +1,143 @@ +--[[pod_format="raw",created="2024-05-24 19:25:58",modified="2024-05-25 20:32:35",revision=1534]] +--[[ + + Drop management + + There is a main and a secondary gem for each drop. + The player control the main gem movement and the secondary gem position: + TOP, BOTTOM, LEFT, RIGHT. + +]] + +Drop = {} + +function pick_random_gem_or_crasher() + return rnd({ + GREEN_GEM, BLUE_GEM, RED_GEM, YELLOW_GEM, + GREEN_CRASHER, BLUE_CRASHER, RED_CRASHER, YELLOW_CRASHER, + }) +end + +function Drop:new(board) + local o = { + board = board, + faster = false, + grace = false, -- A short period of time where you can move even if blocked + grace_delay = 0, + main = Gem:new(pick_random_gem_or_crasher()), + secondary = (board.drop_counter % 25 == 0) and Gem:new(RAINBOW_CRASHER) or Gem:new(pick_random_gem_or_crasher()), + secondary_position = TOP, + x = board.drop_start_position, + y = 1, + } + + setmetatable(o, self) + self.__index = self + + return o +end + +function Drop:fall() + local speed = self.faster and FAST_SPEED or BASE_SPEED + self.board.drop_speed + + local main_new_x = self.x + local main_new_y = ceil(self.y + speed) + + local x, y = self:secondary_coords() + local secondary_new_x = x + local secondary_new_y = ceil(y + speed) + + if self.board:position_is_valid(main_new_x, main_new_y) and self.board:position_is_valid(secondary_new_x, secondary_new_y) then + self.grace = false -- If we are able to go down after a grace started, we must cancel it + self.y += speed + else + self.y = ceil(self.y) -- Ensure the grace is visualy complete (no 1px remaining) + if not self.grace then + self.grace = true + self.grace_delay = GRACE_DELAY + end + end +end + +function Drop:secondary_coords() + local x = self.x + local y = self.y + + if self.secondary_position == TOP then + y -= 1 + end + + if self.secondary_position == BOTTOM then + y += 1 + end + + if self.secondary_position == LEFT then + x += 1 + end + + if self.secondary_position == RIGHT then + x -= 1 + end + + return x, y +end + +function Drop:move_left() + local sec_x, sec_y = self:secondary_coords() + + if self.x > 1 and sec_x > 1 then + self.x -= 1 + end +end + +function Drop:move_right() + local sec_x, sec_y = self:secondary_coords() + + if self.x < self.board.board_width and sec_x < self.board.board_width then + self.x += 1 + end +end + +function Drop:run_grace_delay() + if not self.grace then return end + + self.grace_delay -= 0.1 +end + +function Drop:rotate_clockwise() + local ori = self.secondary_position + + self.secondary_position += 1 + if self.secondary_position > 4 then self.secondary_position = 1 end + + -- @TODO Smart repositioning : slide on the opposite side if possible + + local x, y = self:secondary_coords() + + if not self.board:position_is_valid(flr(x), flr(y)) then + self.secondary_position = ori + end +end + +function Drop:rotate_counter_clockwise() + local ori = self.secondary_position + + self.secondary_position -= 1 + if self.secondary_position < 1 then self.secondary_position = 4 end + + -- @TODO Smart repositioning : slide on the opposite side if possible + + local x, y = self:secondary_coords() + + if not self.board:position_is_valid(flr(x), flr(y)) then + self.secondary_position = ori + end +end + +function Drop:go_faster() + self.faster = true +end + +function Drop:go_normal() + self.faster = false +end \ No newline at end of file diff --git a/src/gem.lua b/src/gem.lua new file mode 100644 index 0000000..66c27d0 --- /dev/null +++ b/src/gem.lua @@ -0,0 +1,53 @@ +--[[pod_format="raw",created="2024-05-24 19:41:08",modified="2024-05-25 20:32:35",revision=1391]] +--[[ + + GEMS + +]] + +Gem = {} + +function Gem:new(type) + o = { + type = type, + exploding = false, + frozen = false, -- If frozen, the gem will not move even if empty spaces are under + } + setmetatable(o, self) + self.__index = self + + return o +end + +function Gem:draw(x, y) + + -- 1..4 are "standard" gems with no animations + -- they are also stored in the sprite sheet at the same index than their value + if self.type < 5 then + spr(self.type, x, y) + end + + -- Others are crashers and need to be animated (in the futur). + -- And the sprite index doesn't correspond to the value. + if self.type == RAINBOW_CRASHER then + spr(8, x, y) + elseif self.type == GREEN_CRASHER then + spr(9, x, y) + elseif self.type == BLUE_CRASHER then + spr(10, x, y) + elseif self.type == RED_CRASHER then + spr(11, x, y) + elseif self.type == YELLOW_CRASHER then + spr(12, x, y) + end + +end + +function Gem:color_match(type) + if (self.type == GREEN_GEM or self.type == GREEN_CRASHER) and (type == GREEN_GEM or type == GREEN_CRASHER) then return true end + if (self.type == BLUE_GEM or self.type == BLUE_CRASHER) and (type == BLUE_GEM or type == BLUE_CRASHER) then return true end + if (self.type == RED_GEM or self.type == RED_CRASHER) and (type == RED_GEM or type == RED_CRASHER) then return true end + if (self.type == YELLOW_GEM or self.type == YELLOW_CRASHER) and (type == YELLOW_GEM or type == YELLOW_CRASHER) then return true end + + return false +end \ No newline at end of file diff --git a/src/state/.info.pod b/src/state/.info.pod new file mode 100644 index 0000000..f4cca64 Binary files /dev/null and b/src/state/.info.pod differ diff --git a/src/state/game.lua b/src/state/game.lua new file mode 100644 index 0000000..4913227 --- /dev/null +++ b/src/state/game.lua @@ -0,0 +1,85 @@ +--[[pod_format="raw",created="2024-05-24 07:55:17",modified="2024-05-25 20:32:35",revision=1823]] +--[[ + + (in) Game state + +]] + +include "src/gem.lua" +include "src/drop.lua" +include "src/board.lua" + +game_state = { + board1 = nil, + board2 = nil, + score1 = 0, + score2 = 0, + mode = MODE_SOLO_ENDLESS, +} + +function game_state:init(options) + + local options = options or { + mode = MODE_SOLO_ENDLESS, + score1 = 0, + score2 = 0, + } + + self.mode = options.mode + self.score1 = options.score1 + self.score2 = options.score2 + + self.board1 = Board:new() + self.board1:new_drop() + + -- Activate the second board when not playing SOLO mode + if self.mode != MODE_SOLO_ENDLESS then + self.board1.side = LEFT_SIDE + self.board2 = Board:new(RIGHT_SIDE) + self.board2:new_drop() + end + +end + +function game_state:draw() + + cls(2) + + -- Display ugly background + spr(192, 0, 0) + + if self.mode == MODE_SOLO_ENDLESS then + self.board1:draw() + else + self.board1:draw(LEFT_SIDE) + self.board2:draw(RIGHT_SIDE) + end + +end + +function game_state:update() + + self:update_board(self.board1) + self:update_board(self.board2) + +end + +function game_state:update_board(board) + if board == nil then return end + + board:apply_physics() + board.drop:run_grace_delay() + board:persist_drop() + + if board.state == BOARD_STATE_RUNNING then + if btn(BTN_P1_DOWN) then board.drop:go_faster() else board.drop:go_normal() end + if btnp(BTN_P1_LEFT) then board.drop:move_left() end + if btnp(BTN_P1_RIGHT) then board.drop:move_right() end + if btnp(BTN_P1_LROT) then board.drop:rotate_counter_clockwise() end + if btnp(BTN_P1_RROT) then board.drop:rotate_clockwise() end + end +end + +function game_state:draw_board(board) + +end \ No newline at end of file diff --git a/src/state/main_menu.lua b/src/state/main_menu.lua new file mode 100644 index 0000000..95fec52 --- /dev/null +++ b/src/state/main_menu.lua @@ -0,0 +1,17 @@ +--[[pod_format="raw",created="2024-05-24 08:10:07",modified="2024-05-24 08:22:48",revision=71]] +main_menu_state = {} + +function main_menu_state:init() + +end + +function main_menu_state:draw() + cls(6) +end + +function main_menu_state:update() + if btnp(BTN_P1_LROT) or btnp(BTN_P1_RROT) then + game_state:init() + _state = game_state + end +end \ No newline at end of file diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..32585f3 --- /dev/null +++ b/todo.txt @@ -0,0 +1,37 @@ +--[[pod_format="raw",created="2024-05-24 08:39:04",modified="2024-05-24 08:49:22",revision=21]] +** Basic game engine ** + +- Menu + - [ ] Add a title screen + - [ ] Dircetly jump to game by pressing a button +- In game + - [ ] Display simple version of the board + - Falling gems + - [ ] Generate falling gems + - [ ] Display the falling gems + - [ ] Handle the falling gems rotation + - [ ] Handle the falling gems movement + - [ ] Handle the falling gems collisions + - [ ] Handle the falling gems final position + - [ ] Handle the crasher gem + - [ ] Add scoring + - [ ] Handle the end of the game (gems reach the top) + + +** Things to do later ** + +- Menu + - [ ] Select difficulty +- In game + - Characters + - [ ] Add characters on the side of the board + - [ ] Animate characters depending on the game + - [ ] Animate crashers + - [ ] Visualy group gems of the same color on the board + - [ ] Reflects effect on the gems + - [ ] Gems explosions + - [ ] Counter blocks + - [ ] P vs P +- Net + - Local network play (using IP?) + - Online play (write a server? leaderboard?) \ No newline at end of file