diff --git a/libs/inspect.lua b/libs/inspect.lua
new file mode 100644
index 0000000..f687a7e
--- /dev/null
+++ b/libs/inspect.lua
@@ -0,0 +1,335 @@
+local inspect ={
+ _VERSION = 'inspect.lua 3.1.0',
+ _URL = 'http://github.com/kikito/inspect.lua',
+ _DESCRIPTION = 'human-readable representations of tables',
+ _LICENSE = [[
+ MIT LICENSE
+
+ Copyright (c) 2013 Enrique GarcĂa Cota
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ ]]
+ }
+
+ local tostring = tostring
+
+ inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
+ inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
+
+ local function rawpairs(t)
+ return next, t, nil
+ end
+
+ -- Apostrophizes the string if it has quotes, but not aphostrophes
+ -- Otherwise, it returns a regular quoted string
+ local function smartQuote(str)
+ if str:match('"') and not str:match("'") then
+ return "'" .. str .. "'"
+ end
+ return '"' .. str:gsub('"', '\\"') .. '"'
+ end
+
+ -- \a => '\\a', \0 => '\\0', 31 => '\31'
+ local shortControlCharEscapes = {
+ ["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
+ ["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
+ }
+ local longControlCharEscapes = {} -- \a => nil, \0 => \000, 31 => \031
+ for i=0, 31 do
+ local ch = string.char(i)
+ if not shortControlCharEscapes[ch] then
+ shortControlCharEscapes[ch] = "\\"..i
+ longControlCharEscapes[ch] = string.format("\\%03d", i)
+ end
+ end
+
+ local function escape(str)
+ return (str:gsub("\\", "\\\\")
+ :gsub("(%c)%f[0-9]", longControlCharEscapes)
+ :gsub("%c", shortControlCharEscapes))
+ end
+
+ local function isIdentifier(str)
+ return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
+ end
+
+ local function isSequenceKey(k, sequenceLength)
+ return type(k) == 'number'
+ and 1 <= k
+ and k <= sequenceLength
+ and math.floor(k) == k
+ end
+
+ local defaultTypeOrders = {
+ ['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
+ ['function'] = 5, ['userdata'] = 6, ['thread'] = 7
+ }
+
+ local function sortKeys(a, b)
+ local ta, tb = type(a), type(b)
+
+ -- strings and numbers are sorted numerically/alphabetically
+ if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
+
+ local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
+ -- Two default types are compared according to the defaultTypeOrders table
+ if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
+ elseif dta then return true -- default types before custom ones
+ elseif dtb then return false -- custom types after default ones
+ end
+
+ -- custom types are sorted out alphabetically
+ return ta < tb
+ end
+
+ -- For implementation reasons, the behavior of rawlen & # is "undefined" when
+ -- tables aren't pure sequences. So we implement our own # operator.
+ local function getSequenceLength(t)
+ local len = 1
+ local v = rawget(t,len)
+ while v ~= nil do
+ len = len + 1
+ v = rawget(t,len)
+ end
+ return len - 1
+ end
+
+ local function getNonSequentialKeys(t)
+ local keys, keysLength = {}, 0
+ local sequenceLength = getSequenceLength(t)
+ for k,_ in rawpairs(t) do
+ if not isSequenceKey(k, sequenceLength) then
+ keysLength = keysLength + 1
+ keys[keysLength] = k
+ end
+ end
+ table.sort(keys, sortKeys)
+ return keys, keysLength, sequenceLength
+ end
+
+ local function countTableAppearances(t, tableAppearances)
+ tableAppearances = tableAppearances or {}
+
+ if type(t) == 'table' then
+ if not tableAppearances[t] then
+ tableAppearances[t] = 1
+ for k,v in rawpairs(t) do
+ countTableAppearances(k, tableAppearances)
+ countTableAppearances(v, tableAppearances)
+ end
+ countTableAppearances(getmetatable(t), tableAppearances)
+ else
+ tableAppearances[t] = tableAppearances[t] + 1
+ end
+ end
+
+ return tableAppearances
+ end
+
+ local copySequence = function(s)
+ local copy, len = {}, #s
+ for i=1, len do copy[i] = s[i] end
+ return copy, len
+ end
+
+ local function makePath(path, ...)
+ local keys = {...}
+ local newPath, len = copySequence(path)
+ for i=1, #keys do
+ newPath[len + i] = keys[i]
+ end
+ return newPath
+ end
+
+ local function processRecursive(process, item, path, visited)
+ if item == nil then return nil end
+ if visited[item] then return visited[item] end
+
+ local processed = process(item, path)
+ if type(processed) == 'table' then
+ local processedCopy = {}
+ visited[item] = processedCopy
+ local processedKey
+
+ for k,v in rawpairs(processed) do
+ processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
+ if processedKey ~= nil then
+ processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
+ end
+ end
+
+ local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
+ if type(mt) ~= 'table' then mt = nil end -- ignore not nil/table __metatable field
+ setmetatable(processedCopy, mt)
+ processed = processedCopy
+ end
+ return processed
+ end
+
+
+
+ -------------------------------------------------------------------
+
+ local Inspector = {}
+ local Inspector_mt = {__index = Inspector}
+
+ function Inspector:puts(...)
+ local args = {...}
+ local buffer = self.buffer
+ local len = #buffer
+ for i=1, #args do
+ len = len + 1
+ buffer[len] = args[i]
+ end
+ end
+
+ function Inspector:down(f)
+ self.level = self.level + 1
+ f()
+ self.level = self.level - 1
+ end
+
+ function Inspector:tabify()
+ self:puts(self.newline, string.rep(self.indent, self.level))
+ end
+
+ function Inspector:alreadyVisited(v)
+ return self.ids[v] ~= nil
+ end
+
+ function Inspector:getId(v)
+ local id = self.ids[v]
+ if not id then
+ local tv = type(v)
+ id = (self.maxIds[tv] or 0) + 1
+ self.maxIds[tv] = id
+ self.ids[v] = id
+ end
+ return tostring(id)
+ end
+
+ function Inspector:putKey(k)
+ if isIdentifier(k) then return self:puts(k) end
+ self:puts("[")
+ self:putValue(k)
+ self:puts("]")
+ end
+
+ function Inspector:putTable(t)
+ if t == inspect.KEY or t == inspect.METATABLE then
+ self:puts(tostring(t))
+ elseif self:alreadyVisited(t) then
+ self:puts('
')
+ elseif self.level >= self.depth then
+ self:puts('{...}')
+ else
+ if self.tableAppearances[t] > 1 then self:puts('<', self:getId(t), '>') end
+
+ local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
+ local mt = getmetatable(t)
+
+ self:puts('{')
+ self:down(function()
+ local count = 0
+ for i=1, sequenceLength do
+ if count > 0 then self:puts(',') end
+ self:puts(' ')
+ self:putValue(t[i])
+ count = count + 1
+ end
+
+ for i=1, nonSequentialKeysLength do
+ local k = nonSequentialKeys[i]
+ if count > 0 then self:puts(',') end
+ self:tabify()
+ self:putKey(k)
+ self:puts(' = ')
+ self:putValue(t[k])
+ count = count + 1
+ end
+
+ if type(mt) == 'table' then
+ if count > 0 then self:puts(',') end
+ self:tabify()
+ self:puts(' = ')
+ self:putValue(mt)
+ end
+ end)
+
+ if nonSequentialKeysLength > 0 or type(mt) == 'table' then -- result is multi-lined. Justify closing }
+ self:tabify()
+ elseif sequenceLength > 0 then -- array tables have one extra space before closing }
+ self:puts(' ')
+ end
+
+ self:puts('}')
+ end
+ end
+
+ function Inspector:putValue(v)
+ local tv = type(v)
+
+ if tv == 'string' then
+ self:puts(smartQuote(escape(v)))
+ elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
+ tv == 'cdata' or tv == 'ctype' then
+ self:puts(tostring(v))
+ elseif tv == 'table' then
+ self:putTable(v)
+ else
+ self:puts('<', tv, ' ', self:getId(v), '>')
+ end
+ end
+
+ -------------------------------------------------------------------
+
+ function inspect.inspect(root, options)
+ options = options or {}
+
+ local depth = options.depth or math.huge
+ local newline = options.newline or '\n'
+ local indent = options.indent or ' '
+ local process = options.process
+
+ if process then
+ root = processRecursive(process, root, {}, {})
+ end
+
+ local inspector = setmetatable({
+ depth = depth,
+ level = 0,
+ buffer = {},
+ ids = {},
+ maxIds = {},
+ newline = newline,
+ indent = indent,
+ tableAppearances = countTableAppearances(root)
+ }, Inspector_mt)
+
+ inspector:putValue(root)
+
+ return table.concat(inspector.buffer)
+ end
+
+ setmetatable(inspect, { __call = function(_, ...) return inspect.inspect(...) end })
+
+ return inspect
+
+
\ No newline at end of file
diff --git a/load/inspect.lua b/load/inspect.lua
new file mode 100644
index 0000000..a811647
--- /dev/null
+++ b/load/inspect.lua
@@ -0,0 +1 @@
+table.inspect = require "libs.inspect"
\ No newline at end of file
diff --git a/load/save.lua b/load/save.lua
index c658171..c0c405b 100644
--- a/load/save.lua
+++ b/load/save.lua
@@ -35,6 +35,27 @@ function initConfig()
if config.secret == nil then config.secret = false end
if not config.gamesettings then config.gamesettings = {} end
+ if not config.gamemodesettings then config.gamemodesettings = {} end
+
+--[[
+ {
+ marathonA1 = {
+ allowHold = 1,
+ ...
+ }
+ }
+]]
+
+ for _, a in ipairs(game_modes) do
+ -- load config options for every mode
+ local mc = {}
+ local conf = a:provideSettings()
+ for i, j in pairs(conf) do
+ mc[j[1]] = (config.gamemodesettings[a.hash] or {})[j[1]] or 1
+ end
+ config.gamemodesettings[a.hash] = mc
+ end
+
for _, option in ipairs(GameConfigScene.options) do
if not config.gamesettings[option[1]] then
config.gamesettings[option[1]] = 1
diff --git a/main.lua b/main.lua
index a322c6d..bb45e20 100644
--- a/main.lua
+++ b/main.lua
@@ -9,6 +9,7 @@ function love.load()
require "load.save"
require "load.bigint"
require "load.version"
+ require "load.inspect"
loadSave()
require "funcs"
require "scene"
@@ -30,14 +31,19 @@ function love.load()
-- used for screenshots
GLOBAL_CANVAS = love.graphics.newCanvas()
+ -- import custom modules
+ initModules()
+
+ -- it is IMPORTANT that this comes before initConfig,
+ -- because modules' configuration options need to be
+ -- known at load-time!!! this will crash if this
+ -- is put back the way it used to be.
+
-- init config
initConfig()
love.window.setFullscreen(config["fullscreen"])
if config.secret then playSE("welcome") end
-
- -- import custom modules
- initModules()
end
function initModules()
diff --git a/scene.lua b/scene.lua
index aaa81b8..efa6d56 100644
--- a/scene.lua
+++ b/scene.lua
@@ -19,3 +19,4 @@ TuningScene = require "scene.tuning"
SettingsScene = require "scene.settings"
CreditsScene = require "scene.credits"
TitleScene = require "scene.title"
+GamemodeConfigScene = require "scene.gamemode_config"
diff --git a/scene/game.lua b/scene/game.lua
index 642b20b..67ca919 100644
--- a/scene/game.lua
+++ b/scene/game.lua
@@ -8,7 +8,7 @@ function GameScene:new(game_mode, ruleset, inputs)
self.retry_mode = game_mode
self.retry_ruleset = ruleset
self.secret_inputs = inputs
- self.game = game_mode(self.secret_inputs)
+ self.game = game_mode(config.gamemodesettings[game_mode.hash], self.secret_inputs)
self.ruleset = ruleset(self.game)
self.game:initialize(self.ruleset)
self.inputs = {
diff --git a/scene/gamemode_config.lua b/scene/gamemode_config.lua
new file mode 100644
index 0000000..7df6de8
--- /dev/null
+++ b/scene/gamemode_config.lua
@@ -0,0 +1,98 @@
+local GamemodeConfigScene = Scene:extend()
+
+GamemodeConfigScene.title = "Gamemode Configuration"
+
+local selected_mode = {}
+local mode_config = {}
+local new_config = {}
+local optioncount = 1
+
+function GamemodeConfigScene:new(gamemode)
+ selected_mode = gamemode
+ mode_config = gamemode:provideSettings() or {}
+ optioncount = #mode_config
+
+ self.highlight = 1
+
+ for i, j in pairs(mode_config) do
+ new_config[j[1]] = config.gamemodesettings[selected_mode.hash][j[1]] or 1
+ end
+
+ DiscordRPC:update({
+ details = "In menus",
+ state = "Configuring "..selected_mode.name,
+ })
+end
+
+function GamemodeConfigScene:save()
+ config.gamemodesettings[selected_mode.hash] = new_config
+end
+
+function GamemodeConfigScene:render()
+ love.graphics.setColor(1, 1, 1, 1)
+ love.graphics.draw(
+ backgrounds["game_config"],
+ 0, 0, 0,
+ 0.5, 0.5
+ )
+
+ love.graphics.setFont(font_3x5_4)
+ love.graphics.print(string.upper(selected_mode.name).." SETTINGS", 80, 40)
+ love.graphics.setFont(font_3x5_2)
+
+ if #mode_config == 0 then
+ love.graphics.print("This mode does not offer any settings.\n"..
+ "Press Backspace to return to mode select.", 40, 100)
+ return
+ end
+
+ love.graphics.setColor(1, 1, 1, 0.5)
+ love.graphics.rectangle("fill", 25, 98 + self.highlight * 20, 170, 22)
+
+ for i, option in ipairs(mode_config) do
+ love.graphics.setColor(1, 1, 1, 1)
+ love.graphics.printf(option[2], 40, 100 + i * 20, 150, "left")
+
+ if #option[3] <= 4 then
+ for j, setting in ipairs(option[3]) do
+ love.graphics.setColor(1, 1, 1, new_config[option[1]] == j and 1 or 0.5)
+ love.graphics.printf(setting, 100 + 110 * j, 100 + i * 20, 100, "center")
+ end
+ else
+ love.graphics.setColor(1, 1, 1, 1)
+ love.graphics.printf(option[3][new_config[option[1]]], -- what an indexer
+ 100 + 110 * 1, 100 + i * 20, 100, 'center')
+ end
+ end
+end
+
+function GamemodeConfigScene:onInputPress(e)
+ if e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
+ scene = ModeSelectScene()
+ elseif e.input == "menu_decide" then
+ playSE("mode_decide")
+ self:save()
+ saveConfig()
+ scene = ModeSelectScene()
+ end
+
+ if #mode_config == 0 then return end
+
+ if e.input == "up" or e.scancode == "up" then
+ playSE("cursor")
+ self.highlight = Mod1(self.highlight-1, optioncount)
+ elseif e.input == "down" or e.scancode == "down" then
+ playSE("cursor")
+ self.highlight = Mod1(self.highlight+1, optioncount)
+ elseif e.input == "left" or e.scancode == "left" then
+ playSE("cursor_lr")
+ local option = mode_config[self.highlight]
+ new_config[option[1]] = Mod1(new_config[option[1]]-1, #option[3])
+ elseif e.input == "right" or e.scancode == "right" then
+ playSE("cursor_lr")
+ local option = mode_config[self.highlight]
+ new_config[option[1]] = Mod1(new_config[option[1]]+1, #option[3])
+ end
+end
+
+return GamemodeConfigScene
diff --git a/scene/mode_select.lua b/scene/mode_select.lua
index f4d4c7d..234acb3 100755
--- a/scene/mode_select.lua
+++ b/scene/mode_select.lua
@@ -87,6 +87,12 @@ function ModeSelectScene:onInputPress(e)
playSE("cursor_lr")
elseif e.input == "menu_back" or e.scancode == "delete" or e.scancode == "backspace" then
scene = TitleScene()
+ elseif e.input == "hold" or e.scancode == "tab" then
+ current_mode = self.menu_state.mode
+ current_ruleset = self.menu_state.ruleset
+ config.current_mode = current_mode
+ config.current_ruleset = current_ruleset
+ scene = GamemodeConfigScene(game_modes[self.menu_state.mode])
elseif e.input then
self.secret_inputs[e.input] = true
end
diff --git a/tetris/modes/gamemode.lua b/tetris/modes/gamemode.lua
index 5d1d287..d2c30d5 100644
--- a/tetris/modes/gamemode.lua
+++ b/tetris/modes/gamemode.lua
@@ -15,7 +15,7 @@ GameMode.hash = ""
GameMode.tagline = ""
GameMode.rollOpacityFunction = function(age) return 0 end
-function GameMode:new(secret_inputs)
+function GameMode:new(config, secret_inputs)
self.grid = Grid(10, 24)
self.randomizer = Randomizer()
self.piece = nil
@@ -72,6 +72,8 @@ function GameMode:new(secret_inputs)
self.section_start_time = 0
self.section_times = { [0] = 0 }
self.secondary_section_times = { [0] = 0 }
+
+ self.config = config
end
function GameMode:getARR() return 1 end
@@ -946,4 +948,8 @@ function GameMode:draw(paused)
end
end
+function GameMode:provideSettings()
+ return {}
+end
+
return GameMode
diff --git a/tetris/modes/marathon_a1.lua b/tetris/modes/marathon_a1.lua
index 2f93443..ccec500 100644
--- a/tetris/modes/marathon_a1.lua
+++ b/tetris/modes/marathon_a1.lua
@@ -14,8 +14,8 @@ MarathonA1Game.tagline = "Can you score enough points to reach the title of Gran
-function MarathonA1Game:new()
- MarathonA1Game.super:new()
+function MarathonA1Game:new(cfg)
+ MarathonA1Game.super:new(cfg)
self.roll_frames = 0
self.combo = 1
@@ -35,9 +35,9 @@ function MarathonA1Game:new()
self.additive_gravity = false
self.lock_drop = false
- self.enable_hard_drop = false
- self.enable_hold = false
- self.next_queue_length = 1
+ self.enable_hard_drop = self.config.allowHardDrop == 2
+ self.enable_hold = self.config.allowHold == 2
+ self.next_queue_length = self.config.nextQueue == 2 and 3 or 1
end
function MarathonA1Game:getARE()
@@ -247,4 +247,14 @@ function MarathonA1Game:getHighscoreData()
}
end
+function MarathonA1Game:provideSettings()
+ local t = {
+ {"allowHold", "Allow hold", {"No", "Yes"}},
+ {"nextQueue", "Next pieces", {"One", "Three"}},
+ {"allowHardDrop", "Allow hard drop", {"No", "Yes"}}
+ }
+
+ return t
+end
+
return MarathonA1Game