first commit

This commit is contained in:
Squishy (C6H12O6+NaCl+H2O)
2024-04-11 08:33:58 +07:00
commit d0307c8765
72 changed files with 8974 additions and 0 deletions

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"Lua.diagnostics.globals": [
"love"
]
}

11
conf.lua Normal file
View File

@@ -0,0 +1,11 @@
function love.conf(t)
t.identity = "tromi_ver3"
t.externalstorage=true
-- t.console = true
t.window.title = "Tromi"
t.window.width = 1280
t.window.height = 960
t.window.vsync = false
end

162
funcs.lua Normal file
View File

@@ -0,0 +1,162 @@
function copy(t)
-- returns deep copy of t (as opposed to the shallow copy you get from var = t)
if type(t) ~= "table" then return t end
local meta = getmetatable(t)
local target = {}
for k, v in pairs(t) do target[k] = v end
setmetatable(target, meta)
return target
end
function strTrueValues(tbl)
-- returns a concatenation of all the keys in tbl with value true, separated with spaces
str = ""
for k, v in pairs(tbl) do
if v == true then
str = str .. k .. " "
end
end
return str
end
function frameTime(min, sec, hth)
-- returns a time in frames from a time in minutes-seconds-hundredths format
if min == nil then min = 0 end
if sec == nil then sec = 0 end
if hth == nil then hth = 0 end
return min*3600 + sec*60 + math.ceil(hth * 0.6)
end
function vAdd(v1, v2)
-- returns the sum of vectors v1 and v2
return {
x = v1.x + v2.x,
y = v1.y + v2.y
}
end
function vNeg(v)
-- returns the opposite of vector v
return {
x = -v.x,
y = -v.y
}
end
function formatTime(frames)
-- returns a mm:ss:hh (h=hundredths) representation of the time in frames given
if frames < 0 then return formatTime(0) end
local min, sec, hund
min = math.floor(frames/3600)
sec = math.floor(frames/60) % 60
hund = math.floor(frames/.6) % 100
str = string.format("%02d:%02d.%02d", min, sec, hund)
return str
end
function formatBigNum(number)
-- returns a string representing a number with commas as thousands separator (e.g. 12,345,678)
local s
if type(number) == "number" then
s = string.format("%d", number)
elseif type(number) == "string" then
if not tonumber(number) then
return
else
s = number
end
else
return
end
local pos = Mod1(string.len(s), 3)
return string.sub(s, 1, pos)
.. string.gsub(string.sub(s, pos+1), "(...)", ",%1")
end
function Mod1(n, m)
-- returns a number congruent to n modulo m in the range [1;m] (as opposed to [0;m-1])
return ((n-1) % m) + 1
end
function table.contains(table, element)
for _, value in pairs(table) do
if value == element then
return true
end
end
return false
end
function table.highest(table)
local i = 1
local highest = nil
for key, value in pairs(table) do
if highest == nil then highest = key end
if table[key] > table[highest] then highest = key end
end
return highest
end
function table.lowest(table)
local i = 1
local lowest = nil
for key, value in pairs(table) do
if lowest == nil then lowest = key end
if table[key] < table[lowest] then lowest = key end
end
return lowest
end
function clamp(x, min, max)
if max < min then
min, max = max, min
end
return x < min and min or (x > max and max or x)
end
function drawText(text, x, y, size, orientation, color)
if color == nil then color = {1, 1, 1, 1} end
love.graphics.setFont(tromi_font)
love.graphics.setColor(0, 0, 0, 0.8)
love.graphics.printf(text, x+1, y+1, size*2, orientation, nil, 0.50)
love.graphics.setColor(color)
love.graphics.printf(text, x, y, size*2, orientation, nil, 0.5)
end
function drawBigText(text, x, y, size, orientation, color)
if color == nil then color = {1, 1, 1, 1} end
love.graphics.setFont(big_font)
love.graphics.setColor(0, 0, 0, 0.8)
love.graphics.printf(text, x+1, y+1, size*2, orientation, nil, 0.50)
love.graphics.setColor(color)
love.graphics.printf(text, x, y, size*2, orientation, nil, 0.5)
end
function boolToInt(value)
if value then return 1 else return 0 end
end
function pairsByKeysReverse (t, f)
local a = {}
for n in pairs(t) do table.insert(a, n) end
table.sort(a, f)
local i = #t+1 -- iterator variable
local iter = function () -- iterator function
i = i - 1
if a[i] == nil then return nil
else return a[i], t[a[i]]
end
end
return iter
end
function split_string(inputstr, sep)
if sep == nil then
sep = "%s"
end
local t={}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
table.insert(t, str)
end
return t
end

1154
game/gamemode.lua Normal file

File diff suppressed because it is too large Load Diff

212
game/grid.lua Normal file
View File

@@ -0,0 +1,212 @@
local Object = require 'libs.classic'
local Grid = Object:extend()
local empty = { skin = "", colour = "", oob=false }
local oob = { skin = "", colour = "", oob=true }
local block = { skin = "2tie", colour = "A", oob=false }
function Grid:new(width, height)
self.grid = {}
self.grid_age = {}
self.width = width
self.height = height
for y = 1, self.height do
self.grid[y] = {}
self.grid_age[y] = {}
for x = 1, self.width do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:clear()
for y = 1, self.height do
for x = 1, self.width do
self.grid[y][x] = empty
self.grid_age[y][x] = 0
end
end
end
function Grid:getCell(x, y)
if x < 1 or x > self.width or y > self.height then return oob
elseif y < 1 then return oob
else return self.grid[y][x]
end
end
function Grid:isOccupied(x, y)
return self:getCell(x+1, y+1) ~= empty
end
function Grid:isRowFull(row)
for index, square in pairs(self.grid[row]) do
if square == empty then return false end
end
return true
end
function Grid:canPlacePiece(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if self:isOccupied(x, y) then
return false
end
end
return true
end
function Grid:canPlacePieceInVisibleGrid(piece)
local offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if y < 1 or self:isOccupied(x, y) ~= empty then
return false
end
end
return true
end
function Grid:getClearedRowCount()
local count = 0
local cleared_row_table = {}
for row = 1, self.height do
if self:isRowFull(row) then
count = count + 1
table.insert(cleared_row_table, row)
end
end
return count, cleared_row_table
end
function Grid:markClearedRows()
local block_table = {}
for row = 1, self.height do
if self:isRowFull(row) then
block_table[row] = {}
for x = 1, self.width do
block_table[row][x] = {
skin = self.grid[row][x].skin,
colour = self.grid[row][x].colour,
}
self.grid[row][x] = {
skin = self.grid[row][x].skin,
colour = "X"
}
end
end
end
return block_table
end
function Grid:clearClearedRows()
for row = 1, self.height do
if self:isRowFull(row) then
for above_row = row, 2, -1 do
self.grid[above_row] = self.grid[above_row - 1]
self.grid_age[above_row] = self.grid_age[above_row - 1]
end
self.grid[1] = {}
self.grid_age[1] = {}
for i = 1, self.width do
self.grid[1][i] = empty
self.grid_age[1][i] = 0
end
end
end
return true
end
function Grid:clearSpecificRow(row)
for col = 1, self.width do
self.grid[row][col] = empty
end
end
function Grid:applyPiece(piece)
if piece.big then
self:applyBigPiece(piece)
return
end
offsets = piece:getBlockOffsets()
for index, offset in pairs(offsets) do
x = piece.position.x + offset.x
y = piece.position.y + offset.y
if y + 1 > 0 and y < self.height then
self.grid[y+1][x+1] = {
skin = piece.skin,
colour = piece.colour,
flash = 5
}
end
end
end
function Grid:checkStackHeight()
for i = 0, self.height - 1 do
for j = 0, self.width - 1 do
if self:isOccupied(j, i) then return self.height - i end
end
end
return 0
end
function Grid:applyMap(map)
for y, row in pairs(map) do
for x, block in pairs(row) do
self.grid_age[y][x] = 0
self.grid[y][x] = block
end
end
end
function Grid:update()
for y = 1, self.height do
for x = 1, self.width do
if self.grid[y][x] ~= empty then
self.grid_age[y][x] = self.grid_age[y][x] + 1
end
end
end
end
function Grid:draw(greyscale, timer)
love.graphics.setColor(0,0,0,1)
love.graphics.rectangle("fill", 256, 31, 80, 45, 0, 0)
love.graphics.setColor(0.3, 0.3, 0.3, 1)
love.graphics.line(256,31,256,31+45)
love.graphics.line(256,31,256+80,31)
love.graphics.line(256+80,31,256+80,31+45)
love.graphics.line(256,31+45,256+80,31+45)
for y = 1, self.height do
for x = 1, self.width do
if blocks[self.grid[y][x].skin] and
blocks[self.grid[y][x].skin][self.grid[y][x].colour] then
if self.grid[y][x].flash ~= nil then
if self.grid[y][x].flash > 0 then
love.graphics.setColor(0.4+(self.grid[y][x].flash*0.1), 0.4+(self.grid[y][x].flash*0.1), 0.4+(self.grid[y][x].flash*0.1), 1)
love.graphics.draw(blocks[self.grid[y][x].skin]['W'], 200+x*16, 64+y*16)
self.grid[y][x].flash = self.grid[y][x].flash - 1
else
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(blocks[self.grid[y][x].skin][self.grid[y][x].colour..'_d'], 200+x*16, 64+y*16)
end
end
if greyscale then
if timer > 1 then timer = 1 end
love.graphics.setColor(0.7, 0.7, 0.7, 0+timer)
if self.grid[y][x] ~= empty and self.grid[y][x].colour ~= 'X' then
love.graphics.draw(blocks[self.grid[y][x].skin]["A"], 200+x*16, 64+y*16)
end
end
end
end
end
end
return Grid

174
game/piece.lua Normal file
View File

@@ -0,0 +1,174 @@
local Object = require 'libs.classic'
local Piece = Object:extend()
function Piece:new(shape, rotation, position, block_offsets, gravity, lock_delay, skin, colour, big)
self.shape = shape
self.rotation = rotation
self.position = position
self.block_offsets = block_offsets
self.gravity = gravity
self.lock_delay = lock_delay
self.skin = skin
self.colour = colour
self.lowest_point = -1
-- self.ghost = false
self.locked = false
-- self.big = big
end
-- Functions that return a new piece to test in rotation systems.
function Piece:withOffset(offset)
return Piece(
self.shape, self.rotation,
{x = self.position.x + offset.x, y = self.position.y + offset.y},
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour
)
end
function Piece:withRelativeRotation(rot)
local new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
return Piece(
self.shape, new_rot, self.position,
self.block_offsets, self.gravity, self.lock_delay, self.skin, self.colour
)
end
-- Functions that return predicates relative to a grid.
function Piece:getBlockOffsets()
return self.block_offsets[self.shape][self.rotation + 1]
end
function Piece:occupiesSquare(x, y)
local offsets = self:getBlockOffsets()
for index, offset in pairs(offsets) do
local new_offset = {x = self.position.x + offset.x, y = self.position.y + offset.y}
if new_offset.x == x and new_offset.y == y then
return true
end
end
return false
end
function Piece:isMoveBlocked(grid, offset)
local moved_piece = self:withOffset(offset)
return not grid:canPlacePiece(moved_piece)
end
function Piece:isDropBlocked(grid)
return self:isMoveBlocked(grid, { x=0, y=1 })
end
-- Procedures to actually do stuff to pieces.
function Piece:setOffset(offset)
self.position.x = self.position.x + offset.x
self.position.y = self.position.y + offset.y
return self
end
function Piece:setRelativeRotation(rot)
new_rot = self.rotation + rot
while new_rot < 0 do new_rot = new_rot + 4 end
while new_rot >= 4 do new_rot = new_rot - 4 end
self.rotation = new_rot
return self
end
function Piece:moveInGrid(step, squares, grid, instant)
local moved = false
for x = 1, squares do
if grid:canPlacePiece(self:withOffset(step)) then
moved = true
self:setOffset(step)
if instant then
self:dropToBottom(grid)
end
else
break
end
end
return self
end
function Piece:dropSquares(dropped_squares, grid)
self:moveInGrid({ x = 0, y = 1 }, dropped_squares, grid)
end
function Piece:dropToBottom(grid)
local piece_y = self.position.y
self:dropSquares(math.huge, grid)
self.gravity = 0
if self.position.y > piece_y then
--self.lock_delay = 0
end
return self
end
function Piece:lockIfBottomed(grid)
if self:isDropBlocked(grid) then
self.locked = true
end
return self
end
function Piece:addGravity(gravity, grid, classic_lock)
gravity = gravity / (self.big and 2 or 1)
local new_gravity = self.gravity + gravity
if self:isDropBlocked(grid) then
if classic_lock then
self.gravity = new_gravity
else
self.gravity = 0
self.lock_delay = self.lock_delay + 1
end
elseif not (
self:isMoveBlocked(grid, { x=0, y=-1 }) and gravity < 0
) then
local dropped_squares = math.floor(math.abs(new_gravity))
if gravity >= 0 then
local new_frac_gravity = new_gravity - dropped_squares
self.gravity = new_frac_gravity
self:dropSquares(dropped_squares, grid)
if self:isDropBlocked(grid) then
playSE("bottom")
end
else
local new_frac_gravity = new_gravity + dropped_squares
self.gravity = new_frac_gravity
self:moveInGrid({ x=0, y=-1 }, dropped_squares, grid)
if self:isMoveBlocked(grid, { x=0, y=-1 }) then
playSE("bottom")
end
end
else
self.gravity = 0
end
return self
end
-- Procedures for drawing.
function Piece:draw(opacity, brightness, grid, partial_das)
if opacity == nil then opacity = 1 end
if brightness == nil then brightness = 1 end
love.graphics.setColor(brightness, brightness, brightness, opacity)
local offsets = self:getBlockOffsets()
local gravity_offset = 0
if partial_das == nil then partial_das = 0 end
for index, offset in pairs(offsets) do
local x = self.position.x + offset.x
local y = self.position.y + offset.y
love.graphics.draw(
blocks[self.skin][self.colour],
216+x*16+partial_das, 80+y*16+gravity_offset
)
end
return false
end
return Piece

148
game/randomizer.lua Normal file
View File

@@ -0,0 +1,148 @@
local Object = require 'libs.classic'
local Randomizer = Object:extend()
local highest_count = 0
function Randomizer:new(always, seed)
self.always = always
if seed ~= nil then
self.seed = seed
else
self.seed = love.math.random(1, 9007199254740991)
end
self.drought = {
I = 0,
J = 0,
L = 0,
T = 0,
S = 0,
Z = 0,
O = 0
}
self.droughted_deals = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
self.piece_counts = {
I = 0,
J = 0,
L = 0,
T = 0,
S = 0,
Z = 0,
O = 0
}
self.piece_round = 0
self:initialize()
end
function Randomizer:nextPiece()
return self:generatePiece()
end
function Randomizer:runTest()
local i_distance = 0
local highest_distance = 0
local distance_total = 0
local i_appearances = 0
local dupe = 0
local trip = 0
local quad = 0
local quin = 0
local lastpiece = 0
local lastpiece2 = 0
local lastpiece3 = 0
local lastpiece4 = 0
local pieceseq_rep = 0
local pieceseq = {}
local last_pieceseq = {}
local highest_discrepancy = 0
local discrepancy = self.piece_counts[table.highest(self.piece_counts)] - self.piece_counts[table.lowest(self.piece_counts)]
local total_chance = 0
for i=0, 750000 do
piece = self:generatePiece()
if #pieceseq < 7 then
table.insert(pieceseq, piece)
else
--print(table.concat(pieceseq))
if table.concat(pieceseq) == table.concat(last_pieceseq) then
pieceseq_rep = pieceseq_rep + 1
end
last_pieceseq = copy(pieceseq)
pieceseq = {}
end
if piece == lastpiece then dupe = dupe + 1 end
if piece == lastpiece and piece == lastpiece2 then trip = trip + 1 end
if piece == lastpiece and piece == lastpiece2 and piece == lastpiece3 then quad = quad + 1 end
if piece == lastpiece and piece == lastpiece2 and piece == lastpiece3 and piece == lastpiece4 then quin = quin + 1 end
if piece == 'I' then
distance_total = distance_total + i_distance
i_appearances = i_appearances + 1
i_distance = 0
else i_distance = i_distance + 1 end
if highest_distance < i_distance then highest_distance = i_distance end
lastpiece4 = lastpiece3
lastpiece3 = lastpiece2
lastpiece2 = lastpiece
lastpiece = piece
--if self.piece_round <= 750 then print(discrepancy) end
local new_discrepancy = self.piece_counts[table.highest(self.piece_counts)] - self.piece_counts[table.lowest(self.piece_counts)]
if new_discrepancy > discrepancy then highest_discrepancy = new_discrepancy end
discrepancy = new_discrepancy
local chance = 0
for piece,drought in pairs(self.drought) do
local piece_chance = 0.14/( ((self.drought[table.highest(self.drought)]+1)-drought) / (self.drought[table.highest(self.drought)]) )
chance = chance + piece_chance
end
chance = chance / 7
total_chance = total_chance + chance
end
--something = something / 750000
print(string.format('dupes: %d, trips: %s, quads: %s, quins: %s, highest i distance: %d, average i distance: %f, pieceseq reps: %d, highest discrepancy:%d, average chance:%f\ndrought lengths dealt: %s', dupe, trip, quad, quin, highest_distance, distance_total/i_appearances, pieceseq_rep, highest_discrepancy, total_chance/750000, table.concat(self.droughted_deals, '-')))
for piece,count in pairs(self.piece_counts) do
print(piece..' '..count)
end
end
function Randomizer:initialize()
local start_pieces = {"I", "J", "L", "T"}
local shapes = {"I", "J", "L", "T", "S", "Z", "O"}
love.math.setRandomSeed(self.seed)
self.drought[table.remove(shapes, love.math.random(1, 4))] = 10
for i=4,9 do
self.drought[table.remove(shapes, love.math.random(1, #shapes))] = i
end
--self:runTest()
end
function Randomizer:generatePiece(always)
if self.always then
return "I"
end
local shapes = {"I", "J", "L", "T", "S", "Z", "O"}
local new_piece = shapes[love.math.random(1,7)]
local chance = love.math.random(0,self.drought[table.highest(self.drought)])
while self.drought[new_piece] < chance do
new_piece = shapes[love.math.random(1,7)]
end
for piece,drought in pairs(self.drought) do
if drought >= 10 then
new_piece = piece
end
end
generated_piece = new_piece
for piece,drought in pairs(self.drought) do
if new_piece ~= piece then self.drought[piece] = self.drought[piece] + 1
else
if drought < #self.droughted_deals then self.droughted_deals[drought+1] = self.droughted_deals[drought+1] + 1 end
self.piece_counts[piece] = self.piece_counts[piece] + 1
self.drought[piece] = 0
end
end
self.piece_round = self.piece_round + 1
return generated_piece
end
return Randomizer

299
game/rotation.lua Normal file
View File

@@ -0,0 +1,299 @@
local Object = require 'libs.classic'
local Piece = require 'game.piece'
local Rotation = Object:extend()
Rotation.spawn_positions = {
I = { x=3, y= -1 },
J = { x=3, y= -1 },
L = { x=3, y= -1 },
O = { x=3, y= -1 },
S = { x=3, y= -1 },
T = { x=3, y= -1 },
Z = { x=3, y= -1 },
}
Rotation.colourscheme = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
Rotation.block_offsets = {
I={
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=3, y=1} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3} },
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=3, y=1} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3} },
},
J={
{ {x=0, y=1}, {x=1, y=1}, {x=2, y=1}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=0, y=2} },
{ {x=0, y=1}, {x=0, y=2}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=0}, {x=2, y=0}, {x=1, y=1}, {x=1, y=2} },
},
L={
{ {x=0, y=1}, {x=0, y=2}, {x=1, y=1}, {x=2, y=1} },
{ {x=0, y=0}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2} },
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=1}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
},
O={
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=2, y=2} },
},
S={
{ {x=1, y=1}, {x=2, y=1}, {x=0, y=2}, {x=1, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2} },
{ {x=1, y=1}, {x=2, y=1}, {x=0, y=2}, {x=1, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2} },
},
T={
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=1} },
{ {x=0, y=1}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2} },
{ {x=0, y=2}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
},
Z={
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=2, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=2, y=2} },
{ {x=2, y=0}, {x=1, y=1}, {x=2, y=1}, {x=1, y=2} },
}
}
Rotation.pieces = 7
-- Component functions.
function Rotation:new(game_mode)
self.game = require 'game.gamemode'
end
function Rotation:rotatePiece(inputs, piece, grid, prev_inputs, initial, lastdir)
local new_inputs = {}
for input, value in pairs(inputs) do
if value and not prev_inputs[input] then
new_inputs[input] = true
end
end
local was_drop_blocked = piece:isDropBlocked(grid)
if self:canPieceRotate(piece, grid) then
-- if not self.held_rotate then
-- self:attemptRotate(inputs, piece, grid, initial)
-- self.held_rotate = true
-- else
self:attemptRotate(new_inputs, piece, grid, initial, lastdir)
-- end
end
if not initial and not was_drop_blocked and piece:isDropBlocked(grid) then
playSE("bottom")
end
-- prev_inputs becomes the previous inputs
for input, value in pairs(inputs) do
prev_inputs[input] = inputs[input]
end
end
function Rotation:attemptRotate(new_inputs, piece, grid, initial, lastdir)
local rot_dir = 0
if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then
rot_dir = 3
if lastdir == 0 then lastdir = -1 end
elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then
rot_dir = 1
if lastdir == 0 then lastdir = 1 end
end
if rot_dir == 0 then return end
local new_piece = piece:withRelativeRotation(rot_dir)
self:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
end
function Rotation:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
-- wallkick routine designed to maximize flexibility while minimizing teleports
-- O doesn't kick
if (piece.shape == "O") then return end
-- assess precisely what rows/columns would be blocked given the desired rotation
local sides = {top=false,uright=false,lright=false,uleft=false,lleft=false,center=false,bottom=false}
local left_exists = false
local right_exists = false
local kick = {x=0,y=0}
for _,offset in pairs(new_piece:getBlockOffsets()) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if offset.x == 0 then left_exists = true end
if offset.x == 2 or offset.x == 3 then right_exists = true end
if grid:isOccupied(x,y) then
if offset.y == 0 then sides.top = true end
if offset.y == 3 then sides.bottom = true end
if offset.y == 1 and offset.x == 0 then sides.uleft = true end
if offset.y == 2 and offset.x == 0 then sides.lleft = true end
if offset.y == 1 and (offset.x == 2 or offset.x == 3) then sides.uright = true end
if offset.y == 2 and (offset.x == 2 or offset.x == 3) then sides.lright = true end
if offset.x == 1 then sides.center = true end
end
end
if sides.top then kick = {x=0,y=1}
elseif (sides.uleft and sides.lright) or (sides.uright and sides.lleft) or (sides.uright and sides.uleft) then kick = {x=0,y=1}
elseif (sides.lleft and sides.lright) then kick = {x=0,y=-1}
elseif (sides.lleft or sides.uleft) then kick = {x=1,y=0}
elseif (sides.lright or sides.uright) then kick = {x=-1,y=0}
elseif sides.center and left_exists then kick = {x=1,y=0}
elseif sides.center and right_exists then kick = {x=-1,y=0}
elseif sides.bottom then kick = {x=0,y=-1}
end
if grid:canPlacePiece(new_piece:withOffset({x=0,y=0})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=0,y=0})
elseif grid:canPlacePiece(new_piece:withOffset(kick)) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset(kick)
end
end
function Rotation:movePiece(piece, grid, move, instant)
local was_drop_blocked = piece:isDropBlocked(grid)
local offset = ({x=0, y=0})
local moves = 0
local y = piece.position.y
if move == "left" then
offset.x = -1
moves = 1
elseif move == "right" then
offset.x = 1
moves = 1
elseif move == "speedleft" then
offset.x = -1
moves = grid.width
elseif move == "speedright" then
offset.x = 1
moves = grid.width
end
if not self:canPieceMove(piece, grid) then return end
for i = 1, moves do
local x = piece.position.x
if moves ~= 1 then
piece:moveInGrid(offset, 1, grid, instant)
else
piece:moveInGrid(offset, 1, grid, false)
end
if piece.position.x ~= x then
self:onPieceMove(piece, grid)
if piece.locked then break end
end
end
if not was_drop_blocked and piece:isDropBlocked(grid) then
playSE("bottom")
end
end
function Rotation:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
local y = piece.position.y
if inputs["down"] == true and drop_locked == false then
piece:addGravity(math.max(gravity, drop_speed), grid, classic_lock)
else
piece:addGravity(gravity, grid, classic_lock)
end
if piece.position.y ~= y then
self:onPieceDrop(piece, grid)
end
end
function Rotation:lockPiece(piece, grid, lock_delay, classic_lock)
if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then
piece.locked = true
end
end
function Rotation:get180RotationValue() return 3 end
function Rotation:getDefaultOrientation() return 1 end
function Rotation:getDrawOffset(shape, orientation) return { x=0, y=0 } end
function Rotation:getAboveFieldOffset(shape, orientation)
if shape == "I" then
return 1
else
return 2
end
end
function Rotation:initializePiece(
inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, big, irs, lastdir
)
local spawn_positions
spawn_positions = self.spawn_positions
local colours
colours = self.colourscheme
self.last_dir = 0
self.held_rotate = false
if inputs['left'] then self.last_dir = -1
elseif inputs['right'] then self.last_dir = 1 end
local spawn_x = math.floor(spawn_positions[data.shape].x / 10 * grid.width)
local spawn_dy
spawn_dy = 0
local piece = Piece(data.shape, data.orientation - 1, {
x = spawn_x or spawn_positions[data.shape].x,
y = spawn_positions[data.shape].y - spawn_dy
}, self.block_offsets, 0, 0, data.skin, colours[data.shape], big)
self:onPieceCreate(piece)
if irs then
self:rotatePiece(inputs, piece, grid, {}, true, lastdir)
end
return piece
end
function Rotation:onPieceCreate(piece) end
function Rotation:processPiece(
inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock, lastdir
)
self:rotatePiece(inputs, piece, grid, prev_inputs, false, lastdir)
self:movePiece(piece, grid, move, gravity >= grid.height)
self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
self:lockPiece(piece, grid, lock_delay, classic_lock)
end
function Rotation:canPieceMove(piece, grid) return true end
function Rotation:canPieceRotate(piece, grid) return true end
function Rotation:onPieceMove(piece) end
function Rotation:onPieceRotate(piece) end
function Rotation:onPieceDrop(piece)
if piece.position.y > piece.lowest_point then
piece.lock_delay = 0
piece.lowest_point = piece.position.y
end
end
return Rotation

287
game/rotation_pent.lua Normal file
View File

@@ -0,0 +1,287 @@
local Object = require 'libs.classic'
local Piece = require 'game.piece'
local Rotation = Object:extend()
Rotation.spawn_positions = {
I = { x=3, y= -2 },
J = { x=3, y= -2 },
L = { x=3, y= -2 },
O = { x=3, y= -2 },
S = { x=3, y= -2 },
T = { x=3, y= -2 },
Z = { x=3, y= -2 },
}
Rotation.colourscheme = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
Rotation.block_offsets = {
I={
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2}, {x=4, y=2} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3}, {x=2, y=4} },
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2}, {x=4, y=2} },
{ {x=2, y=0}, {x=2, y=1}, {x=2, y=2}, {x=2, y=3}, {x=2, y=4} },
},
J={
{ {x=0, y=2}, {x=1, y=2}, {x=2, y=2}, {x=2, y=3}, {x=3, y=2} },
{ {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=0, y=2}, {x=1, y=3} },
{ {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=3, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=2, y=1}, {x=1, y=2}, {x=1, y=3}, {x=1, y=0} },
},
L={
{ {x=0, y=2}, {x=0, y=3}, {x=1, y=2}, {x=2, y=2}, {x=3, y=2} },
{ {x=0, y=0}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=1, y=3}, {x=2, y=3}, {x=3, y=2}, {x=3, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=1, y=0} },
},
O={
{ {x=0, y=2}, {x=0, y=3}, {x=1, y=2}, {x=2, y=2}, {x=1, y=3} },
{ {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=0, y=2} },
{ {x=0, y=3}, {x=1, y=3}, {x=2, y=2}, {x=2, y=3}, {x=1, y=2} },
{ {x=1, y=1}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=2, y=2} },
},
S={
{ {x=1, y=2}, {x=2, y=2}, {x=0, y=3}, {x=1, y=3}, {x=3, y=2} },
{ {x=0, y=0}, {x=0, y=1}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=2, y=2}, {x=3, y=2}, {x=1, y=3}, {x=2, y=3}, {x=0, y=3} },
{ {x=1, y=1}, {x=1, y=2}, {x=2, y=2}, {x=2, y=3}, {x=1, y=0} },
},
T={
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=2}, {x=3, y=2} },
{ {x=0, y=1}, {x=1, y=0}, {x=1, y=1}, {x=1, y=2}, {x=1, y=3} },
{ {x=1, y=3}, {x=2, y=2}, {x=2, y=3}, {x=3, y=3}, {x=0, y=3} },
{ {x=2, y=1}, {x=2, y=2}, {x=3, y=2}, {x=2, y=3}, {x=2, y=0} },
},
Z={
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=2, y=2} },
{ {x=1, y=1}, {x=0, y=2}, {x=1, y=2}, {x=0, y=3}, {x=1, y=3} },
{ {x=0, y=2}, {x=1, y=2}, {x=1, y=3}, {x=2, y=3}, {x=0, y=3} },
{ {x=2, y=1}, {x=1, y=2}, {x=2, y=2}, {x=1, y=3}, {x=1, y=1} },
}
}
Rotation.pieces = 7
-- Component functions.
function Rotation:new(game_mode)
self.game = require 'game.gamemode'
end
function Rotation:rotatePiece(inputs, piece, grid, prev_inputs, initial, lastdir)
local new_inputs = {}
for input, value in pairs(inputs) do
if value and not prev_inputs[input] then
new_inputs[input] = true
end
end
local was_drop_blocked = piece:isDropBlocked(grid)
if self:canPieceRotate(piece, grid) then
-- if not self.held_rotate then
-- self:attemptRotate(inputs, piece, grid, initial)
-- self.held_rotate = true
-- else
self:attemptRotate(new_inputs, piece, grid, initial, lastdir)
-- end
end
if not initial and not was_drop_blocked and piece:isDropBlocked(grid) then
playSE("bottom")
end
-- prev_inputs becomes the previous inputs
for input, value in pairs(inputs) do
prev_inputs[input] = inputs[input]
end
end
function Rotation:attemptRotate(new_inputs, piece, grid, initial, lastdir)
local rot_dir = 0
if (new_inputs["rotate_left"] or new_inputs["rotate_left2"]) then
rot_dir = 3
if lastdir == 0 then lastdir = -1 end
elseif (new_inputs["rotate_right"] or new_inputs["rotate_right2"]) then
rot_dir = 1
if lastdir == 0 then lastdir = 1 end
end
if rot_dir == 0 then return end
local new_piece = piece:withRelativeRotation(rot_dir)
self:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
end
function Rotation:attemptWallkicks(piece, new_piece, rot_dir, grid, lastdir)
-- assess precisely what rows/columns would be blocked given the desired rotation
local sides = {top=false,uright=false,lright=false,uleft=false,lleft=false,center=false,bottom=false}
local left_exists = false
local right_exists = false
local kick = {x=0,y=0}
for _,offset in pairs(new_piece:getBlockOffsets()) do
local x = piece.position.x + offset.x
local y = piece.position.y + offset.y
if grid:isOccupied(x,y) then
if offset.y <= 1 then sides.top = true end
if offset.y == 4 then sides.bottom = true end
if offset.x <= 1 then sides.lleft = true end
if offset.x >= 2 then sides.lright = true end
end
end
if sides.top then kick = {x=0,y=1}
elseif sides.bottom then kick = {x=0,y=-1}
elseif (sides.lleft or sides.uleft) then kick = {x=1,y=0}
elseif (sides.lright or sides.uright) then kick = {x=-1,y=0}
end
if grid:canPlacePiece(new_piece:withOffset({x=0,y=0})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=0,y=0})
elseif grid:canPlacePiece(new_piece:withOffset(kick)) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset(kick)
elseif grid:canPlacePiece(new_piece:withOffset({x=kick.x*2,y=kick.y*2})) then
self:onPieceRotate(piece, grid)
piece:setRelativeRotation(rot_dir):setOffset({x=kick.x*2,y=kick.y*2})
end
end
function Rotation:movePiece(piece, grid, move, instant)
local was_drop_blocked = piece:isDropBlocked(grid)
local offset = ({x=0, y=0})
local moves = 0
local y = piece.position.y
if move == "left" then
offset.x = -1
moves = 1
elseif move == "right" then
offset.x = 1
moves = 1
elseif move == "speedleft" then
offset.x = -1
moves = grid.width
elseif move == "speedright" then
offset.x = 1
moves = grid.width
end
if not self:canPieceMove(piece, grid) then return end
for i = 1, moves do
local x = piece.position.x
if moves ~= 1 then
piece:moveInGrid(offset, 1, grid, instant)
else
piece:moveInGrid(offset, 1, grid, false)
end
if piece.position.x ~= x then
self:onPieceMove(piece, grid)
if piece.locked then break end
end
end
if not was_drop_blocked and piece:isDropBlocked(grid) then
playSE("bottom")
end
end
function Rotation:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
local y = piece.position.y
if inputs["down"] == true and drop_locked == false then
piece:addGravity(math.max(gravity, drop_speed), grid, classic_lock)
else
piece:addGravity(gravity, grid, classic_lock)
end
if piece.position.y ~= y then
self:onPieceDrop(piece, grid)
end
end
function Rotation:lockPiece(piece, grid, lock_delay, classic_lock)
if piece:isDropBlocked(grid) and piece.lock_delay >= lock_delay then
piece.locked = true
end
end
function Rotation:get180RotationValue() return 3 end
function Rotation:getDefaultOrientation() return 1 end
function Rotation:getDrawOffset(shape, orientation) return { x=0, y=0 } end
function Rotation:getAboveFieldOffset(shape, orientation)
if shape == "I" then
return 1
else
return 2
end
end
function Rotation:initializePiece(
inputs, data, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked, big, irs, lastdir
)
local spawn_positions
spawn_positions = self.spawn_positions
local colours
colours = self.colourscheme
self.last_dir = 0
self.held_rotate = false
if inputs['left'] then self.last_dir = -1
elseif inputs['right'] then self.last_dir = 1 end
local spawn_x = math.floor(spawn_positions[data.shape].x / 10 * grid.width)
local spawn_dy
spawn_dy = 0
local piece = Piece(data.shape, data.orientation - 1, {
x = spawn_x or spawn_positions[data.shape].x,
y = spawn_positions[data.shape].y - spawn_dy
}, self.block_offsets, 0, 0, data.skin, colours[data.shape], big)
self:onPieceCreate(piece)
if irs then
self:rotatePiece(inputs, piece, grid, {}, true, lastdir)
end
return piece
end
function Rotation:onPieceCreate(piece) end
function Rotation:processPiece(
inputs, piece, grid, gravity, prev_inputs,
move, lock_delay, drop_speed,
drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock, lastdir
)
self:rotatePiece(inputs, piece, grid, prev_inputs, false, lastdir)
self:movePiece(piece, grid, move, gravity >= grid.height)
self:dropPiece(
inputs, piece, grid, gravity, drop_speed, drop_locked, hard_drop_locked,
hard_drop_enabled, additive_gravity, classic_lock
)
self:lockPiece(piece, grid, lock_delay, classic_lock)
end
function Rotation:canPieceMove(piece, grid) return true end
function Rotation:canPieceRotate(piece, grid) return true end
function Rotation:onPieceMove(piece) end
function Rotation:onPieceRotate(piece) end
function Rotation:onPieceDrop(piece)
if piece.position.y > piece.lowest_point then
piece.lock_delay = 0
piece.lowest_point = piece.position.y
end
end
return Rotation

566
libs/bigint/bigint.lua Normal file
View File

@@ -0,0 +1,566 @@
#!/usr/bin/env lua
-- If this variable is true, then strict type checking is performed for all
-- operations. This may result in slower code, but it will allow you to catch
-- errors and bugs earlier.
local strict = false
--------------------------------------------------------------------------------
local bigint = {}
local mt = {
__add = function(lhs, rhs)
return bigint.add(lhs, rhs)
end,
__unm = function(arg)
return bigint.negate(arg)
end,
__sub = function(lhs, rhs)
return bigint.subtract(lhs, rhs)
end,
__mul = function(lhs, rhs)
return bigint.multiply(lhs, rhs)
end,
__div = function(lhs, rhs)
return bigint.divide(lhs, rhs)
end,
__mod = function(lhs, rhs)
return bigint.modulus(lhs, rhs)
end,
__pow = function(lhs, rhs)
return bigint.exponentiate(lhs, rhs)
end,
__tostring = function(arg)
return bigint.unserialize(arg, "s")
end,
__eq = function(lhs, rhs)
return bigint.compare(lhs, rhs, "==")
end,
__lt = function(lhs, rhs)
return bigint.compare(lhs, rhs, "<")
end,
__le = function(lhs, rhs)
return bigint.compare(lhs, rhs, "<=")
end
}
local named_powers = require("libs.bigint.named-powers-of-ten")
-- Create a new bigint or convert a number or string into a big
-- Returns an empty, positive bigint if no number or string is given
function bigint.new(num)
local self = {
sign = "+",
digits = {}
}
-- Return a new bigint with the same sign and digits
function self:clone()
local newint = bigint.new()
newint.sign = self.sign
for _, digit in pairs(self.digits) do
newint.digits[#newint.digits + 1] = digit
end
return newint
end
setmetatable(self, mt)
if (num) then
local num_string = tostring(num)
for digit in string.gmatch(num_string, "[0-9]") do
table.insert(self.digits, tonumber(digit))
end
if string.sub(num_string, 1, 1) == "-" then
self.sign = "-"
end
end
return self
end
-- Check the type of a big
-- Normally only runs when global variable "strict" == true, but checking can be
-- forced by supplying "true" as the second argument.
function bigint.check(big, force)
if (strict or force) then
assert(getmetatable(big) == mt, "at least one arg is not a bigint")
assert(#big.digits > 0, "bigint is empty")
assert(big.sign == "+" or big.sign == "-", "bigint is unsigned")
for _, digit in pairs(big.digits) do
assert(type(digit) == "number", "at least one digit is invalid")
assert(digit <= 9 and digit >= 0, digit .. " is not between 0 and 9")
assert(math.floor(digit) == digit, digit .. " is not an integer")
end
end
return true
end
-- Return a new big with the same digits but with a positive sign (absolute
-- value)
function bigint.abs(big)
bigint.check(big)
local result = big:clone()
result.sign = "+"
return result
end
-- Return a new big with the same digits but the opposite sign (negation)
function bigint.negate(big)
bigint.check(big)
local result = big:clone()
if (result.sign == "+") then
result.sign = "-"
else
result.sign = "+"
end
return result
end
-- Return the number of digits in the big
function bigint.digits(big)
bigint.check(big)
return #big.digits
end
-- Convert a big to a number or string
function bigint.unserialize(big, output_type, precision)
bigint.check(big)
local num = ""
if big.sign == "-" then
num = "-"
end
if ((output_type == nil)
or (output_type == "number")
or (output_type == "n")
or (output_type == "string")
or (output_type == "s")) then
-- Unserialization to a string or number requires reconstructing the
-- entire number
for _, digit in pairs(big.digits) do
num = num .. math.floor(digit) -- lazy way of getting rid of .0$
end
if ((output_type == nil)
or (output_type == "number")
or (output_type == "n")) then
return tonumber(num)
else
return num
end
else
-- Unserialization to human-readable form or scientific notation only
-- requires reading the first few digits
if (precision == nil) then
precision = math.min(#big.digits, 3)
else
assert(precision > 0, "Precision cannot be less than 1")
assert(math.floor(precision) == precision,
"Precision must be a positive integer")
end
-- num is the first (precision + 1) digits, the first being separated by
-- a decimal point from the others
num = num .. math.floor(big.digits[1])
if (precision > 1) then
num = num .. "."
for i = 1, (precision - 1) do
num = num .. math.floor(big.digits[i + 1])
end
end
if ((output_type == "human-readable")
or (output_type == "human")
or (output_type == "h"))
and (#big.digits >= 3 and #big.digits <= 10002) then
-- Human-readable output contributed by 123eee555
local name
local walkback = 0 -- Used to enumerate "ten", "hundred", etc
-- Walk backwards in the index of named_powers starting at the
-- number of digits of the input until the first value is found
for i = (#big.digits - 1), (#big.digits - 4), -1 do
name = named_powers[i]
if (name) then
if (walkback == 1) then
name = "ten " .. name
elseif (walkback == 2) then
name = "hundred " .. name
end
break
else
walkback = walkback + 1
end
end
return num .. " " .. name
else
return num .. "*10^" .. (#big.digits - 1)
end
end
end
-- Basic comparisons
-- Accepts symbols (<, >=, ~=) and Unix shell-like options (lt, ge, ne)
function bigint.compare(big1, big2, comparison)
bigint.check(big1)
bigint.check(big2)
local greater = false -- If big1.digits > big2.digits
local equal = false
if (big1.sign == "-") and (big2.sign == "+") then
greater = false
elseif (#big1.digits > #big2.digits)
or ((big1.sign == "+") and (big2.sign == "-")) then
greater = true
elseif (#big1.digits == #big2.digits) then
-- Walk left to right, comparing digits
for digit = 1, #big1.digits do
if (big1.digits[digit] > big2.digits[digit]) then
greater = true
break
elseif (big2.digits[digit] > big1.digits[digit]) then
break
elseif (digit == #big1.digits)
and (big1.digits[digit] == big2.digits[digit]) then
equal = true
end
end
end
-- If both numbers are negative, then the requirements for greater are
-- reversed
if (not equal) and (big1.sign == "-") and (big2.sign == "-") then
greater = not greater
end
return (((comparison == "<") or (comparison == "lt"))
and ((not greater) and (not equal)) and true)
or (((comparison == ">") or (comparison == "gt"))
and ((greater) and (not equal)) and true)
or (((comparison == "==") or (comparison == "eq"))
and (equal) and true)
or (((comparison == ">=") or (comparison == "ge"))
and (equal or greater) and true)
or (((comparison == "<=") or (comparison == "le"))
and (equal or not greater) and true)
or (((comparison == "~=") or (comparison == "!=") or (comparison == "ne"))
and (not equal) and true)
or false
end
-- BACKEND: Add big1 and big2, ignoring signs
function bigint.add_raw(big1, big2)
bigint.check(big1)
bigint.check(big2)
local result = bigint.new()
local max_digits = 0
local carry = 0
if (#big1.digits >= #big2.digits) then
max_digits = #big1.digits
else
max_digits = #big2.digits
end
-- Walk backwards right to left, like in long addition
for digit = 0, max_digits - 1 do
local sum = (big1.digits[#big1.digits - digit] or 0)
+ (big2.digits[#big2.digits - digit] or 0)
+ carry
if (sum >= 10) then
carry = 1
sum = sum - 10
else
carry = 0
end
result.digits[max_digits - digit] = sum
end
-- Leftover carry in cases when #big1.digits == #big2.digits and sum > 10, ex. 7 + 9
if (carry == 1) then
table.insert(result.digits, 1, 1)
end
return result
end
-- BACKEND: Subtract big2 from big1, ignoring signs
function bigint.subtract_raw(big1, big2)
-- Type checking is done by bigint.compare
assert(bigint.compare(bigint.abs(big1), bigint.abs(big2), ">="),
"Size of " .. bigint.unserialize(big1, "string") .. " is less than "
.. bigint.unserialize(big2, "string"))
local result = big1:clone()
local max_digits = #big1.digits
local borrow = 0
-- Logic mostly copied from bigint.add_raw ---------------------------------
-- Walk backwards right to left, like in long subtraction
for digit = 0, max_digits - 1 do
local diff = (big1.digits[#big1.digits - digit] or 0)
- (big2.digits[#big2.digits - digit] or 0)
- borrow
if (diff < 0) then
borrow = 1
diff = diff + 10
else
borrow = 0
end
result.digits[max_digits - digit] = diff
end
----------------------------------------------------------------------------
-- Strip leading zeroes if any, but not if 0 is the only digit
while (#result.digits > 1) and (result.digits[1] == 0) do
table.remove(result.digits, 1)
end
return result
end
-- FRONTEND: Addition and subtraction operations, accounting for signs
function bigint.add(big1, big2)
-- Type checking is done by bigint.compare
local result
-- If adding numbers of different sign, subtract the smaller sized one from
-- the bigger sized one and take the sign of the bigger sized one
if (big1.sign ~= big2.sign) then
if (bigint.compare(bigint.abs(big1), bigint.abs(big2), ">")) then
result = bigint.subtract_raw(big1, big2)
result.sign = big1.sign
else
result = bigint.subtract_raw(big2, big1)
result.sign = big2.sign
end
elseif (big1.sign == "+") and (big2.sign == "+") then
result = bigint.add_raw(big1, big2)
elseif (big1.sign == "-") and (big2.sign == "-") then
result = bigint.add_raw(big1, big2)
result.sign = "-"
end
return result
end
function bigint.subtract(big1, big2)
-- Type checking is done by bigint.compare in bigint.add
-- Subtracting is like adding a negative
local big2_local = big2:clone()
if (big2.sign == "+") then
big2_local.sign = "-"
else
big2_local.sign = "+"
end
return bigint.add(big1, big2_local)
end
-- BACKEND: Multiply a big by a single digit big, ignoring signs
function bigint.multiply_single(big1, big2)
bigint.check(big1)
bigint.check(big2)
assert(#big2.digits == 1, bigint.unserialize(big2, "string")
.. " has more than one digit")
local result = bigint.new()
local carry = 0
-- Logic mostly copied from bigint.add_raw ---------------------------------
-- Walk backwards right to left, like in long multiplication
for digit = 0, #big1.digits - 1 do
local this_digit = big1.digits[#big1.digits - digit]
* big2.digits[1]
+ carry
if (this_digit >= 10) then
carry = math.floor(this_digit / 10)
this_digit = this_digit - (carry * 10)
else
carry = 0
end
result.digits[#big1.digits - digit] = this_digit
end
-- Leftover carry in cases when big1.digits[1] * big2.digits[1] > 0
if (carry > 0) then
table.insert(result.digits, 1, carry)
end
----------------------------------------------------------------------------
return result
end
-- FRONTEND: Multiply two bigs, accounting for signs
function bigint.multiply(big1, big2)
-- Type checking done by bigint.multiply_single
local result = bigint.new(0)
local larger, smaller -- Larger and smaller in terms of digits, not size
if (bigint.unserialize(big1) == 0) or (bigint.unserialize(big2) == 0) then
return result
end
if (#big1.digits >= #big2.digits) then
larger = big1
smaller = big2
else
larger = big2
smaller = big1
end
-- Walk backwards right to left, like in long multiplication
for digit = 0, #smaller.digits - 1 do
-- Sorry for going over column 80! There's lots of big names here
local this_digit_product = bigint.multiply_single(larger,
bigint.new(smaller.digits[#smaller.digits - digit]))
-- "Placeholding zeroes"
if (digit > 0) then
for placeholder = 1, digit do
table.insert(this_digit_product.digits, 0)
end
end
result = bigint.add(result, this_digit_product)
end
if (larger.sign == smaller.sign) then
result.sign = "+"
else
result.sign = "-"
end
return result
end
-- Raise a big to a positive integer or big power (TODO: negative integer power)
function bigint.exponentiate(big, power)
-- Type checking for big done by bigint.multiply
assert(bigint.compare(power, bigint.new(0), ">="),
" negative powers are not supported")
local exp = power:clone()
if (bigint.compare(exp, bigint.new(0), "==")) then
return bigint.new(1)
elseif (bigint.compare(exp, bigint.new(1), "==")) then
return big:clone()
else
local result = bigint.new(1)
local base = big:clone()
while (true) do
if (bigint.compare(
bigint.modulus(exp, bigint.new(2)), bigint.new(1), "=="
)) then
result = bigint.multiply(result, base)
end
if (bigint.compare(exp, bigint.new(1), "==")) then
break
else
exp = bigint.divide(exp, bigint.new(2))
base = bigint.multiply(base, base)
end
end
return result
end
end
-- BACKEND: Divide two bigs (decimals not supported), returning big result and
-- big remainder
-- WARNING: Only supports positive integers
function bigint.divide_raw(big1, big2)
-- Type checking done by bigint.compare
if (bigint.compare(big1, big2, "==")) then
return bigint.new(1), bigint.new(0)
elseif (bigint.compare(big1, big2, "<")) then
return bigint.new(0), big1:clone()
else
assert(bigint.compare(big2, bigint.new(0), "!="), "error: divide by zero")
assert(big1.sign == "+", "error: big1 is not positive")
assert(big2.sign == "+", "error: big2 is not positive")
local result = bigint.new()
local dividend = bigint.new() -- Dividend of a single operation
local neg_zero = bigint.new(0)
neg_zero.sign = "-"
for i = 1, #big1.digits do
-- Fixes a negative zero bug
if (#dividend.digits ~= 0) and (bigint.compare(dividend, neg_zero, "==")) then
dividend = bigint.new()
end
table.insert(dividend.digits, big1.digits[i])
local factor = bigint.new(0)
while bigint.compare(dividend, big2, ">=") do
dividend = bigint.subtract(dividend, big2)
factor = bigint.add(factor, bigint.new(1))
end
for i = 0, #factor.digits - 1 do
result.digits[#result.digits + 1 - i] = factor.digits[i + 1]
end
end
-- Remove leading zeros from result
while (result.digits[1] == 0) do
table.remove(result.digits, 1)
end
return result, dividend
end
end
-- FRONTEND: Divide two bigs (decimals not supported), returning big result and
-- big remainder, accounting for signs
function bigint.divide(big1, big2)
local result, remainder = bigint.divide_raw(bigint.abs(big1),
bigint.abs(big2))
if (big1.sign == big2.sign) then
result.sign = "+"
else
result.sign = "-"
end
return result, remainder
end
-- FRONTEND: Return only the remainder from bigint.divide
function bigint.modulus(big1, big2)
local result, remainder = bigint.divide(big1, big2)
-- Remainder will always have the same sign as the dividend per C standard
-- https://en.wikipedia.org/wiki/Modulo_operation#Remainder_calculation_for_the_modulo_operation
remainder.sign = big1.sign
return remainder
end
return bigint

File diff suppressed because it is too large Load Diff

689
libs/binser.lua Normal file
View File

@@ -0,0 +1,689 @@
-- binser.lua
--[[
Copyright (c) 2016 Calvin Rose
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 assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack
-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
local log, abs, floor = math.log, math.abs, math.floor
local log2 = log(2)
frexp = function(x)
if x == 0 then return 0, 0 end
local e = floor(log(abs(x)) / log2 + 1)
return x / 2 ^ e, e
end
end
-- NIL = 202
-- FLOAT = 203
-- TRUE = 204
-- FALSE = 205
-- STRING = 206
-- TABLE = 207
-- REFERENCE = 208
-- CONSTRUCTOR = 209
-- FUNCTION = 210
-- RESOURCE = 211
-- INT64 = 212
local mts = {}
local ids = {}
local serializers = {}
local deserializers = {}
local resources = {}
local resources_by_name = {}
local function pack(...)
return {...}, select("#", ...)
end
local function not_array_index(x, len)
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end
local function type_check(x, tp, name)
assert(type(x) == tp,
format("Expected parameter %q to be of type %q.", name, tp))
end
local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
local mtype = math.type
bigIntSupport = loadstring[[
local char = string.char
return function(n)
local nn = n < 0 and -(n + 1) or n
local b1 = nn // 0x100000000000000
local b2 = nn // 0x1000000000000 % 0x100
local b3 = nn // 0x10000000000 % 0x100
local b4 = nn // 0x100000000 % 0x100
local b5 = nn // 0x1000000 % 0x100
local b6 = nn // 0x10000 % 0x100
local b7 = nn // 0x100 % 0x100
local b8 = nn % 0x100
if n < 0 then
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
end]]()
isInteger = function(x)
return mtype(x) == 'integer'
end
else
isInteger = function(x)
return floor(x) == x
end
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
if isInteger(n) then -- int
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
return char(n + 27)
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
n = n + 8192
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
elseif bigIntSupport then
return bigIntSupport(n)
end
end
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local m, e = frexp(n) -- mantissa, exponent
if m ~= m then
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
elseif m == 1/0 then
if sign == 0 then
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
else
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
end
e = e + 0x3FE
if e < 1 then -- denormalized numbers
m = m * 2 ^ (52 + e)
e = 0
else
m = (m * 2 - 1) * 2 ^ 52
end
return char(203,
sign + floor(e / 0x10),
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
floor(m / 0x10000000000) % 0x100,
floor(m / 0x100000000) % 0x100,
floor(m / 0x1000000) % 0x100,
floor(m / 0x10000) % 0x100,
floor(m / 0x100) % 0x100,
m % 0x100)
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
local b = byte(str, index)
if b < 128 then
return b - 27, index + 1
elseif b < 192 then
return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2
end
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
if b == 212 then
local flip = b1 >= 128
if flip then -- negative
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
if flip then
return (-n) - 1, index + 9
else
return n, index + 9
end
end
local sign = b1 > 0x7F and -1 or 1
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
local n
if e == 0 then
if m == 0 then
n = sign * 0.0
else
n = sign * (m / 2 ^ 52) * 2 ^ -1022
end
elseif e == 0x7FF then
if m == 0 then
n = sign * (1/0)
else
n = 0.0/0.0
end
else
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
end
return n, index + 9
end
local types = {}
types["nil"] = function(x, visited, accum)
accum[#accum + 1] = "\202"
end
function types.number(x, visited, accum)
accum[#accum + 1] = number_to_str(x)
end
function types.boolean(x, visited, accum)
accum[#accum + 1] = x and "\204" or "\205"
end
function types.string(x, visited, accum)
local alen = #accum
if visited[x] then
accum[alen + 1] = "\208"
accum[alen + 2] = number_to_str(visited[x])
else
visited[x] = visited.next
visited.next = visited.next + 1
accum[alen + 1] = "\206"
accum[alen + 2] = number_to_str(#x)
accum[alen + 3] = x
end
end
local function check_custom_type(x, visited, accum)
local res = resources[x]
if res then
accum[#accum + 1] = "\211"
types[type(res)](res, visited, accum)
return true
end
local mt = getmetatable(x)
local id = mt and ids[mt]
if id then
if x == visited.temp then
error("Infinite loop in constructor.")
end
visited.temp = x
accum[#accum + 1] = "\209"
types[type(id)](id, visited, accum)
local args, len = pack(serializers[id](x))
accum[#accum + 1] = number_to_str(len)
for i = 1, len do
local arg = args[i]
types[type(arg)](arg, visited, accum)
end
visited[x] = visited.next
visited.next = visited.next + 1
return true
end
end
function types.userdata(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this userdata.")
end
end
function types.table(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local xlen = #x
accum[#accum + 1] = "\207"
accum[#accum + 1] = number_to_str(xlen)
for i = 1, xlen do
local v = x[i]
types[type(v)](v, visited, accum)
end
local key_count = 0
for k in pairs(x) do
if not_array_index(k, xlen) then
key_count = key_count + 1
end
end
accum[#accum + 1] = number_to_str(key_count)
for k, v in pairs(x) do
if not_array_index(k, xlen) then
types[type(k)](k, visited, accum)
types[type(v)](v, visited, accum)
end
end
end
end
types["function"] = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local str = dump(x)
accum[#accum + 1] = "\210"
accum[#accum + 1] = number_to_str(#str)
accum[#accum + 1] = str
end
end
types.cdata = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, #accum) then return end
error("Cannot serialize this cdata.")
end
end
types.thread = function() error("Cannot serialize threads.") end
local function deserialize_value(str, index, visited)
local t = byte(str, index)
if not t then return end
if t < 128 then
return t - 27, index + 1
elseif t < 192 then
return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2
elseif t == 202 then
return nil, index + 1
elseif t == 203 then
return number_from_str(str, index)
elseif t == 204 then
return true, index + 1
elseif t == 205 then
return false, index + 1
elseif t == 206 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local substr = sub(str, dataindex, nextindex - 1)
visited[#visited + 1] = substr
return substr, nextindex
elseif t == 207 then
local count, nextindex = number_from_str(str, index + 1)
local ret = {}
visited[#visited + 1] = ret
for i = 1, count do
ret[i], nextindex = deserialize_value(str, nextindex, visited)
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local k, v
k, nextindex = deserialize_value(str, nextindex, visited)
v, nextindex = deserialize_value(str, nextindex, visited)
ret[k] = v
end
return ret, nextindex
elseif t == 208 then
local ref, nextindex = number_from_str(str, index + 1)
return visited[ref], nextindex
elseif t == 209 then
local count
local name, nextindex = deserialize_value(str, index + 1, visited)
count, nextindex = number_from_str(str, nextindex)
local args = {}
for i = 1, count do
args[i], nextindex = deserialize_value(str, nextindex, visited)
end
local ret = deserializers[name](unpack(args))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 210 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local ret = loadstring(sub(str, dataindex, nextindex - 1))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 211 then
local res, nextindex = deserialize_value(str, index + 1, visited)
return resources_by_name[res], nextindex
elseif t == 212 then
return number_from_str(str, index)
else
error("Could not deserialize type byte " .. t .. ".")
end
end
local function serialize(...)
local visited = {next = 1}
local accum = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
return concat(accum)
end
local function make_file_writer(file)
return setmetatable({}, {
__newindex = function(_, _, v)
file:write(v)
end
})
end
local function serialize_to_file(path, mode, ...)
local file, err = io.open(path, mode)
assert(file, err)
local visited = {next = 1}
local accum = make_file_writer(file)
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
-- flush the writer
file:flush()
file:close()
end
local function writeFile(path, ...)
return serialize_to_file(path, "wb", ...)
end
local function appendFile(path, ...)
return serialize_to_file(path, "ab", ...)
end
local function deserialize(str, index)
assert(type(str) == "string", "Expected string to deserialize.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
return vals, len
end
local function deserializeN(str, n, index)
assert(type(str) == "string", "Expected string to deserialize.")
n = n or 1
assert(type(n) == "number", "Expected a number for parameter n.")
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index and len < n do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
vals[len + 1] = index
return unpack(vals, 1, n + 1)
end
local function readFile(path)
local file, err = io.open(path, "rb")
if file == nil then
return nil, 0
end
local str = file:read("*all")
file:close()
return deserialize(str)
end
local function default_deserialize(metatable)
return function(...)
local ret = {}
for i = 1, select("#", ...), 2 do
ret[select(i, ...)] = select(i + 1, ...)
end
return setmetatable(ret, metatable)
end
end
local function default_serialize(x)
assert(type(x) == "table",
"Default serialization for custom types only works for tables.")
local args = {}
local len = 0
for k, v in pairs(x) do
args[len + 1], args[len + 2] = k, v
len = len + 2
end
return unpack(args, 1, len)
end
-- Templating
local function normalize_template(template)
local ret = {}
for i = 1, #template do
ret[i] = template[i]
end
local non_array_part = {}
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
-- in templates. Looking for way around this.
for k in pairs(template) do
if not_array_index(k, #template) then
non_array_part[#non_array_part + 1] = k
end
end
table.sort(non_array_part)
for i = 1, #non_array_part do
local name = non_array_part[i]
ret[#ret + 1] = {name, normalize_template(template[name])}
end
return ret
end
local function templatepart_serialize(part, argaccum, x, len)
local extras = {}
local extracount = 0
for k, v in pairs(x) do
extras[k] = v
extracount = extracount + 1
end
for i = 1, #part do
extracount = extracount - 1
if type(part[i]) == "table" then
extras[part[i][1]] = nil
len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len)
else
extras[part[i]] = nil
len = len + 1
argaccum[len] = x[part[i]]
end
end
if extracount > 0 then
argaccum[len + 1] = extras
else
argaccum[len + 1] = nil
end
return len + 1
end
local function templatepart_deserialize(ret, part, values, vindex)
for i = 1, #part do
local name = part[i]
if type(name) == "table" then
local newret = {}
ret[name[1]] = newret
vindex = templatepart_deserialize(newret, name[2], values, vindex)
else
ret[name] = values[vindex]
vindex = vindex + 1
end
end
local extras = values[vindex]
if extras then
for k, v in pairs(extras) do
ret[k] = v
end
end
return vindex + 1
end
local function template_serializer_and_deserializer(metatable, template)
return function(x)
argaccum = {}
local len = templatepart_serialize(template, argaccum, x, 0)
return unpack(argaccum, 1, len)
end, function(...)
local ret = {}
local len = select("#", ...)
local args = {...}
templatepart_deserialize(ret, template, args, 1)
return setmetatable(ret, metatable)
end
end
local function register(metatable, name, serialize, deserialize)
name = name or metatable.name
serialize = serialize or metatable._serialize
deserialize = deserialize or metatable._deserialize
if not serialize then
if metatable._template then
local t = normalize_template(metatable._template)
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
elseif not deserialize then
serialize = default_serialize
deserialize = default_deserialize(metatable)
else
serialize = metatable
end
end
type_check(metatable, "table", "metatable")
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert(not ids[metatable], "Metatable already registered.")
assert(not mts[name], ("Name %q already registered."):format(name))
mts[name] = metatable
ids[metatable] = name
serializers[name] = serialize
deserializers[name] = deserialize
return metatable
end
local function unregister(item)
local name, metatable
if type(item) == "string" then -- assume name
name, metatable = item, mts[item]
else -- assume metatable
name, metatable = ids[item], item
end
type_check(name, "string", "name")
type_check(metatable, "table", "metatable")
mts[name] = nil
ids[metatable] = nil
serializers[name] = nil
deserializers[name] = nil
return metatable
end
local function registerClass(class, name)
name = name or class.name
if class.__instanceDict then -- middleclass
register(class.__instanceDict, name)
else -- assume 30log or similar library
register(class, name)
end
return class
end
local function registerResource(resource, name)
type_check(name, "string", "name")
assert(not resources[resource],
"Resource already registered.")
assert(not resources_by_name[name],
format("Resource %q already exists.", name))
resources_by_name[name] = resource
resources[resource] = name
return resource
end
local function unregisterResource(name)
type_check(name, "string", "name")
assert(resources_by_name[name], format("Resource %q does not exist.", name))
local resource = resources_by_name[name]
resources_by_name[name] = nil
resources[resource] = nil
return resource
end
return {
-- aliases
s = serialize,
d = deserialize,
dn = deserializeN,
r = readFile,
w = writeFile,
a = appendFile,
serialize = serialize,
deserialize = deserialize,
deserializeN = deserializeN,
readFile = readFile,
writeFile = writeFile,
appendFile = appendFile,
register = register,
unregister = unregister,
registerResource = registerResource,
unregisterResource = unregisterResource,
registerClass = registerClass
}

68
libs/classic.lua Normal file
View File

@@ -0,0 +1,68 @@
--
-- classic
--
-- Copyright (c) 2014, rxi
--
-- This module is free software; you can redistribute it and/or modify it under
-- the terms of the MIT license. See LICENSE for details.
--
local Object = {}
Object.__index = Object
function Object:new()
end
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
function Object:implement(...)
for _, cls in pairs({...}) do
for k, v in pairs(cls) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
end
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
function Object:__tostring()
return "Object"
end
function Object:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
return Object

165
libs/lualzw.lua Normal file
View File

@@ -0,0 +1,165 @@
--[[
MIT License
Copyright (c) 2016 Rochet2
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 char = string.char
local type = type
local select = select
local sub = string.sub
local tconcat = table.concat
local basedictcompress = {}
local basedictdecompress = {}
for i = 0, 255 do
local ic, iic = char(i), char(i, 0)
basedictcompress[ic] = iic
basedictdecompress[iic] = ic
end
local function dictAddA(str, dict, a, b)
if a >= 256 then
a, b = 0, b+1
if b >= 256 then
dict = {}
b = 1
end
end
dict[str] = char(a,b)
a = a+1
return dict, a, b
end
local function compress(input)
if type(input) ~= "string" then
return nil, "string expected, got "..type(input)
end
local len = #input
if len <= 1 then
return "u"..input
end
local dict = {}
local a, b = 0, 1
local result = {"c"}
local resultlen = 1
local n = 2
local word = ""
for i = 1, len do
local c = sub(input, i, i)
local wc = word..c
if not (basedictcompress[wc] or dict[wc]) then
local write = basedictcompress[word] or dict[word]
if not write then
return nil, "algorithm error, could not fetch word"
end
result[n] = write
resultlen = resultlen + #write
n = n+1
if len <= resultlen then
return "u"..input
end
dict, a, b = dictAddA(wc, dict, a, b)
word = c
else
word = wc
end
end
result[n] = basedictcompress[word] or dict[word]
resultlen = resultlen+#result[n]
n = n+1
if len <= resultlen then
return "u"..input
end
return tconcat(result)
end
local function dictAddB(str, dict, a, b)
if a >= 256 then
a, b = 0, b+1
if b >= 256 then
dict = {}
b = 1
end
end
dict[char(a,b)] = str
a = a+1
return dict, a, b
end
local function decompress(input)
if type(input) ~= "string" then
return nil, "string expected, got "..type(input)
end
if #input < 1 then
return nil, "invalid input - not a compressed string"
end
local control = sub(input, 1, 1)
if control == "u" then
return sub(input, 2)
elseif control ~= "c" then
return nil, "invalid input - not a compressed string"
end
input = sub(input, 2)
local len = #input
if len < 2 then
return nil, "invalid input - not a compressed string"
end
local dict = {}
local a, b = 0, 1
local result = {}
local n = 1
local last = sub(input, 1, 2)
result[n] = basedictdecompress[last] or dict[last]
n = n+1
for i = 3, len, 2 do
local code = sub(input, i, i+1)
local lastStr = basedictdecompress[last] or dict[last]
if not lastStr then
return nil, "could not find last from dict. Invalid input?"
end
local toAdd = basedictdecompress[code] or dict[code]
if toAdd then
result[n] = toAdd
n = n+1
dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b)
else
local tmp = lastStr..sub(lastStr, 1, 1)
result[n] = tmp
n = n+1
dict, a, b = dictAddB(tmp, dict, a, b)
end
last = code
end
return tconcat(result)
end
return {
compress = compress,
decompress = decompress,
}

138
libs/simple-slider.lua Normal file
View File

@@ -0,0 +1,138 @@
--[[
Copyright (c) 2016 George Prosser
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 slider = {}
slider.__index = slider
function newSlider(x, y, length, value, min, max, setter, style)
local s = {}
s.value = (value - min) / (max - min)
s.min = min
s.max = max
s.setter = setter
s.x = x
s.y = y
s.length = length
local p = style or {}
s.width = p.width or length * 0.1
s.orientation = p.orientation or 'horizontal'
s.track = p.track or 'rectangle'
s.knob = p.knob or 'rectangle'
s.grabbed = false
s.wasDown = true
s.ox = 0
s.oy = 0
return setmetatable(s, slider)
end
function slider:update(mouseX, mouseY, mouseDown)
local x = mouseX or love.mouse.getX()
local y = mouseY or love.mouse.getY()
local down = love.mouse.isDown(1)
if mouseDown ~= nil then
down = mouseDown
end
local knobX = self.x
local knobY = self.y
if self.orientation == 'horizontal' then
knobX = self.x - self.length/2 + self.length * self.value
elseif self.orientation == 'vertical' then
knobY = self.y + self.length/2 - self.length * self.value
end
local ox = x - knobX
local oy = y - knobY
local dx = ox - self.ox
local dy = oy - self.oy
if down then
if self.grabbed then
if self.orientation == 'horizontal' then
self.value = self.value + dx / self.length
elseif self.orientation == 'vertical' then
self.value = self.value - dy / self.length
end
elseif (x > knobX - self.width/2 and x < knobX + self.width/2 and y > knobY - self.width/2 and y < knobY + self.width/2) and not self.wasDown then
self.ox = ox
self.oy = oy
self.grabbed = true
end
else
self.grabbed = false
end
self.value = math.max(0, math.min(1, self.value))
if self.setter ~= nil then
self.setter(self.min + self.value * (self.max - self.min))
end
self.wasDown = down
end
function slider:draw()
if self.track == 'rectangle' then
if self.orientation == 'horizontal' then
love.graphics.rectangle('line', self.x - self.length/2 - self.width/2, self.y - self.width/2, self.length + self.width, self.width)
elseif self.orientation == 'vertical' then
love.graphics.rectangle('line', self.x - self.width/2, self.y - self.length/2 - self.width/2, self.width, self.length + self.width)
end
elseif self.track == 'line' then
if self.orientation == 'horizontal' then
love.graphics.line(self.x - self.length/2, self.y, self.x + self.length/2, self.y)
elseif self.orientation == 'vertical' then
love.graphics.line(self.x, self.y - self.length/2, self.x, self.y + self.length/2)
end
elseif self.track == 'roundrect' then
if self.orientation == 'horizontal' then
love.graphics.rectangle('line', self.x - self.length/2 - self.width/2, self.y - self.width/2, self.length + self.width, self.width, self.width/2, self.width)
elseif self.orientation == 'vertical' then
love.graphics.rectangle('line', self.x - self.width/2, self.y - self.length/2 - self.width/2, self.width, self.length + self.width, self.width, self.width/2)
end
end
local knobX = self.x
local knobY = self.y
if self.orientation == 'horizontal' then
knobX = self.x - self.length/2 + self.length * self.value
elseif self.orientation == 'vertical' then
knobY = self.y + self.length/2 - self.length * self.value
end
if self.knob == 'rectangle' then
love.graphics.rectangle('fill', knobX - self.width/2, knobY - self.width/2, self.width, self.width)
elseif self.knob == 'circle' then
love.graphics.circle('fill', knobX, knobY, self.width/2)
end
end
function slider:getValue()
return self.min + self.value * (self.max - self.min)
end

2
load/bigint.lua Normal file
View File

@@ -0,0 +1,2 @@
bigint = require "libs.bigint.bigint"
number_names = require "libs.bigint.named-powers-of-ten"

2
load/fonts.lua Normal file
View File

@@ -0,0 +1,2 @@
tromi_font = love.graphics.newFont('res/fonts/monofonto rg.otf', 28)
big_font = love.graphics.newFont('res/fonts/monofonto rg.otf', 56)

48
load/graphics.lua Normal file
View File

@@ -0,0 +1,48 @@
backgrounds = {
[0] = love.graphics.newVideo("res/backgrounds/green_waterfall.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/water.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/green_streams.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/streams.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/red_forest_waterfall.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/flowers_rain.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/moonlight_tree.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/lisa_frank.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/snowy_trees.ogv", {audio=false}),
love.graphics.newVideo("res/backgrounds/snowy_cabin.ogv", {audio=false}),
}
blocks = {
["2tie"] = {
R = love.graphics.newImage("res/img/r.png"),
O = love.graphics.newImage("res/img/o.png"),
Y = love.graphics.newImage("res/img/y.png"),
G = love.graphics.newImage("res/img/g.png"),
C = love.graphics.newImage("res/img/b.png"),
B = love.graphics.newImage("res/img/i.png"),
M = love.graphics.newImage("res/img/v.png"),
F = love.graphics.newImage("res/img/bl.png"),
A = love.graphics.newImage("res/img/bl.png"),
X = love.graphics.newImage("res/img/t.png"),
W = love.graphics.newImage("res/img/w.png"),
R_d = love.graphics.newImage("res/img/r_d.png"),
O_d = love.graphics.newImage("res/img/o_d.png"),
Y_d = love.graphics.newImage("res/img/y_d.png"),
G_d = love.graphics.newImage("res/img/g_d.png"),
C_d = love.graphics.newImage("res/img/b_d.png"),
B_d = love.graphics.newImage("res/img/i_d.png"),
M_d = love.graphics.newImage("res/img/v_d.png"),
}
}
ColourSchemes = {
Arika = {
I = "R",
L = "O",
J = "B",
S = "M",
Z = "G",
O = "Y",
T = "C",
}
}

29
load/save.lua Normal file
View File

@@ -0,0 +1,29 @@
local binser = require 'libs.binser'
local fs = love.filesystem
function loadSave()
config = loadFromFile(CONFIG_FILE)
end
function loadFromFile(filename)
local save_data = fs.read(filename)
if save_data == nil then
return {} -- new object
end
return binser.deserialize(save_data)[1]
end
function initConfig()
if config.fullscreen == nil then config.fullscreen = false end
if config.music == nil then config.music = true end
if not config.input then
scene = InputConfigScene(true)
else
scene = TitleScene()
end
end
function saveConfig()
fs.write(CONFIG_FILE,binser.serialize(config))
end

49
load/sounds.lua Normal file
View File

@@ -0,0 +1,49 @@
sounds = {
bottom = love.audio.newSource("res/se/bottom.wav", "static"),
lock = love.audio.newSource("res/se/lock.wav", "static"),
erase = love.audio.newSource("res/se/erase.wav", "static"),
fall = love.audio.newSource("res/se/fall.wav", "static"),
ready = love.audio.newSource("res/se/ready.wav", "static"),
promote = love.audio.newSource("res/se/promote.wav", "static"),
demote = love.audio.newSource("res/se/demote.wav", "static"),
autopromote = love.audio.newSource("res/se/autopromote.wav", "static"),
bgm_firsthalf = love.audio.newSource("res/bgm/firsthalf.flac", "static"),
bgm_secondhalf = love.audio.newSource("res/bgm/secondhalf.flac", "static"),
bgm_title = love.audio.newSource("res/bgm/title.flac", "static")
}
function playSE(sound, subsound)
if sound ~= nil then
if subsound ~= nil then
sounds[sound][subsound]:setVolume(0.4)
if sounds[sound][subsound]:isPlaying() then
sounds[sound][subsound]:stop()
end
sounds[sound][subsound]:play()
else
sounds[sound]:setVolume(0.4)
if sounds[sound]:isPlaying() then
sounds[sound]:stop()
end
sounds[sound]:play()
end
end
end
function playSEOnce(sound, subsound)
if sound ~= nil then
if subsound ~= nil then
sounds[sound][subsound]:setVolume(0.4)
if sounds[sound][subsound]:isPlaying() then
return
end
sounds[sound][subsound]:play()
else
sounds[sound]:setVolume(0.4)
if sounds[sound]:isPlaying() then
return
end
sounds[sound]:play()
end
end
end

389
main.lua Normal file
View File

@@ -0,0 +1,389 @@
require 'funcs'
-- savedir = love.filesystem.getSaveDirectory()
-- if love.system.getOS() == 'Windows' then
-- local test_for_separator = string.find(savedir, '/')
-- if test_for_separator ~= nil then savedir = table.concat(split_string(savedir, '/'), '\\') end
-- savedir = savedir..'\\saves\\'
-- configfile = savedir..'config.sav'
-- replaydir = savedir..'replays\\'
-- hiscorefile = savedir..'hiscores.sav'
-- else
-- savedir = savedir..'/saves/'
-- configfile = savedir..'config.sav'
-- replaydir = savedir..'replays/'
-- hiscorefile = savedir..'hiscores.sav'
-- end
SAVE_DIR = '/saves/'
CONFIG_FILE = 'config.sav'
REPLAY_DIR = 'replays/'
HIscoreFILE = 'hiscores.sav'
PENTO_MODE = false
function love.load()
math.randomseed(os.time())
highscores = {}
require "load.graphics"
require "load.fonts"
require "load.sounds"
require "load.save"
require "load.bigint"
loadSave()
require "scene"
love.window.setMode(love.graphics.getWidth(), love.graphics.getHeight(), {resizable = true});
love.mouse.setVisible(false)
love.keyboard.setKeyRepeat(true)
love.keyboard.setTextInput(true)
-- used for screenshots
GLOBAL_CANVAS = love.graphics.newCanvas()
-- init config
initConfig()
love.window.setFullscreen(config["fullscreen"])
end
function love.draw()
love.graphics.setCanvas(GLOBAL_CANVAS)
love.graphics.clear()
love.graphics.push()
-- get offset matrix
local width = love.graphics.getWidth()
local height = love.graphics.getHeight()
local scale_factor = math.min(width / 640, height / 480)
love.graphics.translate(
(width - scale_factor * 640) / 2,
(height - scale_factor * 480) / 2
)
love.graphics.scale(scale_factor)
scene:render()
love.graphics.pop()
drawText("Pressed: "..(lastPressedKey or '[NONE]').." | Released key: "..(lastReleasedKey or '[NONE]'),0,0,1000,"left")
love.graphics.setCanvas()
love.graphics.setColor(1,1,1,1)
love.graphics.draw(GLOBAL_CANVAS)
end
function love.keypressed(key, scancode)
local input_pressed=nil
-- global hotkeys
if scancode == "f4" then
config["fullscreen"] = not config["fullscreen"]
saveConfig()
love.window.setFullscreen(config["fullscreen"])
elseif scancode == "f2" and scene.title ~= "Input Config" and scene.title ~= "Game" then
scene = InputConfigScene()
-- function keys are reserved
elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then
return
-- escape is reserved for menu_back
elseif scancode == "escape" then
scene:onInputPress({input="menu_back", type="key", key=key, scancode=scancode})
-- pass any other key to the scene, with its configured mapping
else
if config.input and config.input.keys then
input_pressed = config.input.keys[scancode]
end
scene:onInputPress({input=input_pressed, type="key", key=key, scancode=scancode})
end
lastPressedKey = input_pressed or scancode
end
function love.keyreleased(key, scancode)
local input_released = nil
-- escape is reserved for menu_back
if scancode == "escape" then
scene:onInputRelease({input="menu_back", type="key", key=key, scancode=scancode})
-- function keys are reserved
elseif string.match(scancode, "^f[1-9]$") or string.match(scancode, "^f[1-9][0-9]+$") then
return
-- handle all other keys; tab is reserved, but the input config scene keeps it from getting configured as a game input, so pass tab to the scene here
else
if config.input and config.input.keys then
input_released = config.input.keys[scancode]
end
scene:onInputRelease({input=input_released, type="key", key=key, scancode=scancode})
end
lastReleasedKey = input_released or scancode
end
function love.joystickpressed(joystick, button)
local input_pressed = nil
if
config.input and
config.input.joysticks and
config.input.joysticks[joystick:getName()] and
config.input.joysticks[joystick:getName()].buttons
then
input_pressed = config.input.joysticks[joystick:getName()].buttons[button]
end
scene:onInputPress({input=input_pressed, type="joybutton", name=joystick:getName(), button=button})
end
function love.joystickreleased(joystick, button)
local input_released = nil
if
config.input and
config.input.joysticks and
config.input.joysticks[joystick:getName()] and
config.input.joysticks[joystick:getName()].buttons
then
input_released = config.input.joysticks[joystick:getName()].buttons[button]
end
scene:onInputRelease({input=input_released, type="joybutton", name=joystick:getName(), button=button})
end
function love.joystickaxis(joystick, axis, value)
local input_pressed = nil
local positive_released = nil
local negative_released = nil
if
config.input and
config.input.joysticks and
config.input.joysticks[joystick:getName()] and
config.input.joysticks[joystick:getName()].axes and
config.input.joysticks[joystick:getName()].axes[axis]
then
if math.abs(value) >= 1 then
input_pressed = config.input.joysticks[joystick:getName()].axes[axis][value >= 1 and "positive" or "negative"]
end
positive_released = config.input.joysticks[joystick:getName()].axes[axis].positive
negative_released = config.input.joysticks[joystick:getName()].axes[axis].negative
end
if math.abs(value) >= 1 then
scene:onInputPress({input=input_pressed, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
else
scene:onInputRelease({input=positive_released, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
scene:onInputRelease({input=negative_released, type="joyaxis", name=joystick:getName(), axis=axis, value=value})
end
end
function love.touchPressed()
love.keyboard.setKeyRepeat(true)
love.keyboard.setTextInput(false)
love.keyboard.setTextInput(true)
end
local last_hat_direction = ""
local directions = {
["u"] = "up",
["d"] = "down",
["l"] = "left",
["r"] = "right",
}
function love.joystickhat(joystick, hat, direction)
local input_pressed = nil
local has_hat = false
if
config.input and
config.input.joysticks and
config.input.joysticks[joystick:getName()] and
config.input.joysticks[joystick:getName()].hats and
config.input.joysticks[joystick:getName()].hats[hat]
then
if direction ~= "c" then
input_pressed = config.input.joysticks[joystick:getName()].hats[hat][direction]
end
has_hat = true
end
if input_pressed then
for i = 1, #direction do
local char = direction:sub(i, i)
local _, count = last_hat_direction:gsub(char, char)
if count == 0 then
scene:onInputPress({input=config.input.joysticks[joystick:getName()].hats[hat][char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
end
end
for i = 1, #last_hat_direction do
local char = last_hat_direction:sub(i, i)
local _, count = direction:gsub(char, char)
if count == 0 then
scene:onInputRelease({input=config.input.joysticks[joystick:getName()].hats[hat][char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
end
end
last_hat_direction = direction
elseif has_hat then
for i, direction in ipairs{"d", "l", "ld", "lu", "r", "rd", "ru", "u"} do
scene:onInputRelease({input=config.input.joysticks[joystick:getName()].hats[hat][direction], type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
end
last_hat_direction = ""
elseif direction ~= "c" then
for i = 1, #direction do
local char = direction:sub(i, i)
local _, count = last_hat_direction:gsub(char, char)
if count == 0 then
scene:onInputPress({input=directions[char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
end
end
for i = 1, #last_hat_direction do
local char = last_hat_direction:sub(i, i)
local _, count = direction:gsub(char, char)
if count == 0 then
scene:onInputRelease({input=directions[char], type="joyhat", name=joystick:getName(), hat=hat, direction=char})
end
end
last_hat_direction = direction
else
for i, direction in ipairs{"d", "l", "ld", "lu", "r", "rd", "ru", "u"} do
scene:onInputRelease({input=nil, type="joyhat", name=joystick:getName(), hat=hat, direction=direction})
end
last_hat_direction = ""
end
end
function love.focus(f)
return
end
function love.resize(w, h)
GLOBAL_CANVAS:release()
GLOBAL_CANVAS = love.graphics.newCanvas(w, h)
end
local TARGET_FPS = 60
function love.run()
if love.load then love.load(love.arg.parseGameArguments(arg), arg) end
if love.timer then love.timer.step() end
local dt = 0
local last_time = love.timer.getTime()
local time_accumulator = 0
return function()
if love.event then
love.event.pump()
for name, a,b,c,d,e,f in love.event.poll() do
if name == "quit" then
if not love.quit or not love.quit() then
return a or 0
end
end
love.handlers[name](a,b,c,d,e,f)
end
end
if scene and scene.update and love.timer then
scene:update()
local frame_duration = 1.0 / TARGET_FPS
if time_accumulator < frame_duration then
if love.graphics and love.graphics.isActive() and love.draw then
love.graphics.origin()
love.graphics.clear(love.graphics.getBackgroundColor())
love.draw()
love.graphics.present()
end
local end_time = last_time + frame_duration
local time = love.timer.getTime()
while time < end_time do
love.timer.sleep(0.001)
time = love.timer.getTime()
end
time_accumulator = time_accumulator + time - last_time
end
time_accumulator = time_accumulator - frame_duration
end
last_time = love.timer.getTime()
end
end
minos = {'R_d', 'O_d', 'Y_d', 'G_d', 'C_d', 'B_d', 'M_d'}
main_bg_grid = {}
for x=1, 40 do
main_bg_grid[x] = {}
for y=1, 30 do
main_bg_grid[x][y] = 0
end
end
main_bg_cur_pos = {20,6}
main_bg_cur_color = minos[love.math.random(1,7)]
main_bg_cur_mino = 1
main_bg_draw_frame = 0
main_bg_last_color = nil
function mainBackground()
if config["music"] and not sounds["bgm_title"]:isPlaying() then
sounds["bgm_title"]:setVolume(0.3)
sounds["bgm_title"]:play()
end
local y = 40
if main_bg_draw_frame >= 16 then
while y > 1 do
for x = 1, 40 do
main_bg_grid[x][y] = main_bg_grid[x][y-1]
end
y = y - 1
end
for x=1, 40 do
main_bg_grid[x][1] = 0
end
main_bg_draw_frame = 0
main_bg_cur_pos[2] = main_bg_cur_pos[2] + 1
end
local directions = { {0,-1},{1,0}, {-1,0}}
local test_dir = directions[love.math.random(1,3)]
main_bg_cur_pos[1] = main_bg_cur_pos[1] + test_dir[1]
main_bg_cur_pos[2] = main_bg_cur_pos[2] + test_dir[2]
if main_bg_cur_pos[1] > 40 then main_bg_cur_pos[1] = 40 end
if main_bg_cur_pos[1] < 1 then main_bg_cur_pos[1] = 1 end
if main_bg_cur_pos[2] > 30 then main_bg_cur_pos[2] = 30 end
if main_bg_cur_pos[2] < 1 then main_bg_cur_pos[2] = 1 end
if main_bg_grid[main_bg_cur_pos[1]][main_bg_cur_pos[2]] == 0 then
main_bg_grid[main_bg_cur_pos[1]][main_bg_cur_pos[2]] = main_bg_cur_color
main_bg_cur_mino = main_bg_cur_mino + 1
end
for x=1,40 do
for y=1,30 do
if main_bg_grid[x][y] ~= 0 then
love.graphics.setColor(1, 1, 1, 0.4)
if ((x-1)*48)-560 > 0 and ((x-1)*48)-560 < 640 then love.graphics.draw(blocks["2tie"][main_bg_grid[x][y]], ((x-1)*48)-570, (((y+2)*48)+main_bg_draw_frame*3)-480,0, 3) end
love.graphics.setColor(1, 1, 1, 1)
end
end
end
for x=1,40 do
for y=1,30 do
if main_bg_grid[x][y] ~= 0 then
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(blocks["2tie"][main_bg_grid[x][y]], (x-1)*16, ((y-1)*16)+main_bg_draw_frame)
end
end
end
for x=1,40 do
for y=1,30 do
if main_bg_grid[x][y] ~= 0 then
love.graphics.setColor(1, 1, 1, 0.6)
if ((x-1)*32)-320 > 0 and ((x-1)*32)-320 < 640 then love.graphics.draw(blocks["2tie"][main_bg_grid[x][y]], ((x-1)*32)-320, (((y+1)*32)+main_bg_draw_frame*2)-320,0, 2) end
love.graphics.setColor(1, 1, 1, 1)
end
end
end
if main_bg_cur_mino == 5 then
--if main_bg_cur_pos[2] < 4 then
-- main_bg_cur_pos[2] = 4
-- main_bg_cur_pos[1] = love.math.random(4, 36)
--end
main_bg_cur_pos = {love.math.random(16,24),6}
main_bg_last_color = main_bg_cur_color
while main_bg_cur_color == main_bg_last_color do main_bg_cur_color = minos[love.math.random(1,7)] end
main_bg_cur_mino = 1
end
main_bg_placed = false
main_bg_draw_frame = main_bg_draw_frame + 1
end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
res/backgrounds/streams.ogv Normal file

Binary file not shown.

BIN
res/backgrounds/water.ogv Normal file

Binary file not shown.

BIN
res/bgm/firsthalf.flac Normal file

Binary file not shown.

BIN
res/bgm/secondhalf.flac Normal file

Binary file not shown.

BIN
res/bgm/title.flac Normal file

Binary file not shown.

BIN
res/fonts/monofonto rg.otf Normal file

Binary file not shown.

BIN
res/img/b.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/b_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/bl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
res/img/g.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/g_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/i.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/i_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/o_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/r.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/r_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/t.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
res/img/v.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/v_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/w.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
res/img/y.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/img/y_d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
res/se/autopromote.wav Normal file

Binary file not shown.

BIN
res/se/bottom.wav Normal file

Binary file not shown.

BIN
res/se/demote.wav Normal file

Binary file not shown.

BIN
res/se/erase.wav Normal file

Binary file not shown.

BIN
res/se/fall.wav Normal file

Binary file not shown.

BIN
res/se/lock.wav Normal file

Binary file not shown.

BIN
res/se/promote.wav Normal file

Binary file not shown.

BIN
res/se/ready.wav Normal file

Binary file not shown.

21
scene.lua Normal file
View File

@@ -0,0 +1,21 @@
local Object = require "libs.classic"
Scene = Object:extend()
function Scene:new() end
function Scene:update() end
function Scene:render() end
function Scene:onInputPress() end
function Scene:onInputRelease() end
ExitScene = require "scene.exit"
GameScene = require "scene.game"
NameEntryScene = require "scene.name_entry"
KeyConfigScene = require "scene.key_config"
StickConfigScene = require "scene.stick_config"
InputConfigScene = require "scene.input_config"
ReplaySelectScene = require "scene.replay"
FullscreenScene = require "scene.fullscreen"
MusicToggleScene = require "scene.music_toggle"
TrainingScene = require "scene.training"
TitleScene = require "scene.title"

23
scene/exit.lua Normal file
View File

@@ -0,0 +1,23 @@
local ExitScene = Scene:extend()
require 'load.save'
ExitScene.title = "Exit Game"
function ExitScene:new()
end
function ExitScene:update()
love.event.quit()
end
function ExitScene:render()
end
function ExitScene:changeOption(rel)
end
function ExitScene:onInputPress(e)
end
return ExitScene

25
scene/fullscreen.lua Normal file
View File

@@ -0,0 +1,25 @@
local FullscreenScene = Scene:extend()
require 'load.save'
FullscreenScene.title = "Fullscreen"
function FullscreenScene:new()
end
function FullscreenScene:update()
config["fullscreen"] = not config["fullscreen"]
saveConfig()
love.window.setFullscreen(config["fullscreen"])
scene = TitleScene()
end
function FullscreenScene:render()
end
function FullscreenScene:changeOption(rel)
end
function FullscreenScene:onInputPress(e)
end
return FullscreenScene

115
scene/game.lua Normal file
View File

@@ -0,0 +1,115 @@
local GameScene = Scene:extend()
local binser = require 'libs.binser'
GameScene.title = "Game"
local tas = false
require 'load.save'
function GameScene:new(player_name, replay_file, replay_grade)
game_mode = require 'game.gamemode'
if PENTO_MODE then
ruleset = require 'game.rotation_pent'
else
ruleset = require 'game.rotation'
end
self.retry_mode = game_mode
self.retry_ruleset = ruleset
self.secret_inputs = inputs
self.reset_stuff = {player_name, replay_file, replay_grade}
self.game = game_mode(player_name, replay_file, replay_grade)
self.ruleset = ruleset(self.game)
self.grace_frames = 0
self.normal_volume = love.audio.getVolume()
self.game:initialize(self.ruleset)
self.inputs = {
left=false,
right=false,
up=false,
down=false,
rotate_left=false,
rotate_left2=false,
rotate_right=false,
rotate_right2=false,
rotate_180=false,
hold=false,
}
self.paused = false
end
function GameScene:update(nosound, tas_update)
local inputs = {}
if tas then
while self.game.are > 2 do
self.game:update(inputs, self.ruleset)
end
end
for input, value in pairs(self.inputs) do
inputs[input] = value
end
if tas and tas_update then
self.paused = false
self.game:update(inputs, self.ruleset)
self.paused = true
return
end
if nosound then
love.audio.setVolume(0)
end
if not nosound and self.grace_frames > 0 then
self.grace_frames = self.grace_frames - 1
if self.grace_frames == 1 then love.audio.setVolume(self.normal_volume) end
end
if not self.paused then
self.game:update(inputs, self.ruleset)
end
end
function GameScene:render()
self.game:draw(self.paused)
end
function GameScene:onInputPress(e)
if (self.game.game_over or self.game.completed) and (e.input == "menu_decide" or e.input == "menu_back" or e.input == "rotate_right") and self.game.game_over_frames > 50 then
scene = TitleScene()
elseif tas and e.input == "menu_decide" then
self:update(false, true)
elseif self.game.input_playback and (e.input == "menu_back") then
scene = TitleScene()
elseif self.game.input_playback and e.input == "rotate_left" then
self.paused = false
self:update()
self.paused = true
elseif self.game.input_playback and e.input == "rotate_right" then
self.paused = false
elseif self.game.input_playback and not self.paused and e.input == 'left' then
local target = self.game.frames - 300
if target < 1 then target = 1 end
self.game = game_mode(self.reset_stuff[1], self.reset_stuff[2], self.reset_stuff[3])
self.ruleset = ruleset(self.game)
self.game:initialize(self.ruleset)
while self.game.frames < (target) do
self:update(true)
end
self.grace_frames = 90
elseif self.game.input_playback and not self.paused and e.input == 'right' then
local target = self.game.frames + 600
if target > #self.game.replay_inputs then target = #self.game.replay_inputs-10 end
self.game = game_mode(self.reset_stuff[1], self.reset_stuff[2], self.reset_stuff[3])
self.ruleset = ruleset(self.game)
self.game:initialize(self.ruleset)
while self.game.frames < (target) do
self:update(true)
end
self.grace_frames = 90
elseif e.input and string.sub(e.input, 1, 5) ~= "menu_" then
self.inputs[e.input] = true
end
end
function GameScene:onInputRelease(e)
if e.input and string.sub(e.input, 1, 5) ~= "menu_" then
self.inputs[e.input] = false
end
end
return GameScene

60
scene/input_config.lua Normal file
View File

@@ -0,0 +1,60 @@
local ConfigScene = Scene:extend()
ConfigScene.title = "Input Config"
local minos = {'R_d', 'O_d', 'Y_d', 'G_d', 'C_d', 'B_d', 'M_d'}
local menu_screens = {
KeyConfigScene,
StickConfigScene
}
function ConfigScene:new(first_time)
self.menu_state = 1
if first_time then
self.first_time = true
else
self.first_time = false
end
end
function ConfigScene:update() end
function ConfigScene:render()
mainBackground()
if not self.first_time then
drawText("Which controls do you want to configure?", 80, 70, 1000)
else
drawText("Thanks for playing Tromi!", 80, 40, 1000)
drawText("Please begin by configuring your controls:", 80, 70, 1000)
end
love.graphics.setColor(1, 1, 1, 0.5)
love.graphics.rectangle("fill", 75, 118 + 50 * self.menu_state, 200, 33)
love.graphics.setColor(1, 1, 1, 1)
for i, screen in pairs(menu_screens) do
drawText(screen.title, 80, 120 + 50 * i, 200, "left")
end
end
function ConfigScene:changeOption(rel)
local len = table.getn(menu_screens)
self.menu_state = (self.menu_state + len + rel - 1) % len + 1
end
function ConfigScene:onInputPress(e)
if e.input == "menu_decide" or e.input == "rotate_left" or e.scancode == "return" then
scene = menu_screens[self.menu_state]()
elseif e.input == "up" or e.scancode == "up" then
self:changeOption(-1)
elseif e.input == "down" or e.scancode == "down" then
self:changeOption(1)
elseif config.input and (
e.input == "menu_back" or e.input == "rotate_right" or e.scancode == "backspace" or e.scancode == "delete"
) then
scene = TitleScene()
end
end
return ConfigScene

100
scene/key_config.lua Normal file
View File

@@ -0,0 +1,100 @@
local KeyConfigScene = Scene:extend()
KeyConfigScene.title = "Key Config"
require 'load.save'
local minos = {'R_d', 'O_d', 'Y_d', 'G_d', 'C_d', 'B_d', 'M_d'}
local configurable_inputs = {
"menu_decide",
"menu_back",
"left",
"right",
"up",
"down",
"rotate_left",
"rotate_left2",
"rotate_right",
"rotate_right2",
}
local input_names = {
menu_decide='Confirm Selection',
menu_back = 'Go Back',
left='Left',
right='Right',
up='Up',
down='Down',
rotate_left='Rotate Counter-clockwise',
rotate_left2='Rotate Counter-clockwise (2)',
rotate_right='Rotate Clockwise',
rotate_right2='Rotate Clockwise (2)'
}
local function newSetInputs()
local set_inputs = {}
for i, input in ipairs(configurable_inputs) do
set_inputs[input] = false
end
return set_inputs
end
function KeyConfigScene:new()
self.input_state = 1
self.set_inputs = newSetInputs()
self.new_input = {}
end
function KeyConfigScene:update()
end
function KeyConfigScene:render()
mainBackground()
for i, input in ipairs(configurable_inputs) do
drawText(input_names[input], 40, 50 + i * 20, 200, "left")
if self.set_inputs[input] then
drawText(self.set_inputs[input], 240, 50 + i * 20, 300, "left")
end
end
if self.input_state > table.getn(configurable_inputs) then
drawText("Press enter to confirm, delete/backspace to retry" .. (config.input and ", escape to cancel" or ""),0,0,1000)
else
drawText("Press key input for " .. input_names[configurable_inputs[self.input_state]] .. ", tab to skip, escape to cancel",0,0,1000)
drawText("Function keys (F1, F2, etc.), escape, and tab can't be changed", 0, 20,1000)
end
end
function KeyConfigScene:onInputPress(e)
if e.type == "key" then
-- function keys, escape, and tab are reserved and can't be remapped
if e.scancode == "escape" then
scene = InputConfigScene()
elseif self.input_state > table.getn(configurable_inputs) then
if e.scancode == "return" then
-- save new input, then load next scene
local had_config = config.input ~= nil
if not config.input then config.input = {} end
config.input.keys = self.new_input
saveConfig()
scene = had_config and InputConfigScene() or TitleScene()
elseif e.scancode == "delete" or e.scancode == "backspace" then
-- retry
self.input_state = 1
self.set_inputs = newSetInputs()
self.new_input = {}
end
elseif e.scancode == "tab" then
self.set_inputs[configurable_inputs[self.input_state]] = "skipped"
self.input_state = self.input_state + 1
elseif e.scancode ~= "escape" and not self.new_input[e.scancode] then
-- all other keys can be configured
self.set_inputs[configurable_inputs[self.input_state]] = "key " .. love.keyboard.getKeyFromScancode(e.scancode) .. " (" .. e.scancode .. ")"
self.new_input[e.scancode] = configurable_inputs[self.input_state]
self.input_state = self.input_state + 1
end
end
end
return KeyConfigScene

24
scene/music_toggle.lua Normal file
View File

@@ -0,0 +1,24 @@
local MusicToggleScene = Scene:extend()
require 'load.save'
MusicToggleScene.title = "Play music during game:"
function MusicToggleScene:new()
end
function MusicToggleScene:update()
config["music"] = not config["music"]
saveConfig()
scene = TitleScene()
end
function MusicToggleScene:render()
end
function MusicToggleScene:changeOption(rel)
end
function MusicToggleScene:onInputPress(e)
end
return MusicToggleScene

191
scene/name_entry.lua Normal file
View File

@@ -0,0 +1,191 @@
local NameEntryScene = Scene:extend()
local Grid = require 'game.grid'
local binser = require 'libs.binser'
require 'load.save'
NameEntryScene.title = "Game Start"
function NameEntryScene:new()
self.chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890."
self.char_pos = 1
self.name_entry = {'A','A','A'}
self.entry_pos = 1
self.entry_chars = self.name_entry[1]..self.name_entry[2]..self.name_entry[3]
self.grid = Grid(10, 20)
self.repeat_limit = 10
self.repeat_counter = self.repeat_limit-1
self.direction = nil
self.grade = 0
self.wins = 0
self.plays = 0
self.delete_confirm = false
self.delete_input_count = 0
self.gradeNames = {
"19k", "18k", "17k", "16k", "15k", "14k", "13k", "12k", "11k",
"10k", "9k", "8k", "7k", "6k", "5k", "4k", "3k", "2k", "1k",
"1D", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D"
}
if config['last_entry'] ~= nil then
self.name_entry = {config['last_entry']:sub(1,1),config['last_entry']:sub(2,2),config['last_entry']:sub(3,3)}
self.entry_pos = 3
end
score_file = io.open(HIscoreFILE, 'rb')
if score_file ~= nil then
self.hi_scores = binser.deserialize(score_file:read('a'))[1]
else
self.hi_scores = {"TRO",0,"MIT",0,"ROM",0,"ITR",0,"OMI",0}
end
end
function NameEntryScene:drawGradeList(left, top)
love.graphics.setColor(0,0,0,0.5)
love.graphics.rectangle("fill", left+3, top+3, 200, 240, 10, 10)
love.graphics.setColor(0.05,0.05,0.05,1)
love.graphics.rectangle("fill", left, top, 200, 240, 10, 10)
drawText("Grade list:", left+15, top+10, 1000, "left")
drawText("Beginner\n19 kyu\n18 kyu\n17 kyu\n16 kyu\n15 kyu\n14 kyu\n13 kyu\n12 kyu\n11 kyu\n10 kyu", left+15, top+25, 1000, "left")
drawText("Intermed.\n9 kyu\n8 kyu\n7 kyu\n6 kyu\n5 kyu\n4 kyu\n3 kyu\n2 kyu\n1 kyu", left+80, top+25, 1000, "left")
drawText("Expert\n1 Dan\n2 Dan\n3 Dan\n4 Dan\n5 Dan\n6 Dan\n7 Dan\n8 Dan\n9 Dan", left+145, top+25, 1000, "left")
end
function NameEntryScene:render()
mainBackground()
love.graphics.setColor(1, 1, 1, 1)
love.graphics.line(216,80,216,80+(16*self.grid.height))
love.graphics.line(216+(16*self.grid.width),80,216+(16*self.grid.width),80+(16*self.grid.height))
love.graphics.line(216,80+(16*self.grid.height),216+(16*self.grid.width),80+(16*self.grid.height))
love.graphics.line(216,80,216+(16*self.grid.width),80)
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle(
"fill", 216, 80,
16 * self.grid.width, 16 * self.grid.height
)
love.graphics.setColor(1, 1, 1, 1)
drawText('Enter your initials:', 227, 180, 200, "left")
drawBigText(self.entry_chars, 272, 200, 200, "left")
drawText('o', 262+(self.entry_pos*14), 225, 200, "left")
self:drawGradeList(397, 40)
love.graphics.setColor(0,0,0,0.5)
love.graphics.rectangle("fill", 400, 295, 130, 130, 10, 10)
love.graphics.setColor(0.05,0.05,0.05,1)
love.graphics.rectangle("fill", 397, 292, 130, 130, 10, 10)
drawText("Best scores:", 410, 297, 1000, "left")
i = 2
while i <= 10 do
drawText(self.hi_scores[i-1]..' - '..self.hi_scores[i], 410, 297+(i*10), 1000, "left")
i = i + 2
end
if self.entry_pos == 4 then
drawText('Press confirm\nto play', 255, 290, 1000)
end
if self.grade > 0 then
drawText(string.format('Games: %s', self.plays), 255, 250, 1000)
drawText(string.format('Grade: %s', self.gradeNames[self.grade]), 255, 270, 1000)
--if not self.delete_confirm then
-- drawText('Press up\nthree times\nto delete', 255, 330, 1000)
--else
-- drawText('Are you sure?\nPress down\nthree times\nto confirm', 255, 330, 1000)
--end
end
end
function NameEntryScene:update()
if self.direction == "left" then
if self.repeat_counter >= self.repeat_limit then
self.char_pos = self.char_pos - 1
if self.char_pos < 1 then self.char_pos = 37 end
self.name_entry[self.entry_pos] = self.chars:sub(self.char_pos, self.char_pos)
self.repeat_counter = 0
end
self.repeat_counter = self.repeat_counter + 1
elseif self.direction == "right" then
if self.repeat_counter >= self.repeat_limit then
self.char_pos = self.char_pos + 1
if self.char_pos > 37 then self.char_pos = 1 end
self.name_entry[self.entry_pos] = self.chars:sub(self.char_pos, self.char_pos)
self.repeat_counter = 0
end
self.repeat_counter = self.repeat_counter + 1
end
self.entry_chars = self.name_entry[1]..self.name_entry[2]..self.name_entry[3]
end
function NameEntryScene:onInputPress(e)
if e.input == "menu_decide" or e.input == "rotate_left" or e.scancode == "return" then
self.delete_confirm = false
self.delete_input_count = 0
if self.entry_pos == 4 then
config['last_entry'] = name:upper()
saveConfig()
scene = GameScene(name:lower())
else
if self.entry_pos == 3 then
name = string.lower(self.name_entry[1]..self.name_entry[2]..self.name_entry[3])
grade_history = io.open(SAVE_DIR..name.."_grade_history.sav", 'rb')
if grade_history ~= nil then
grade_history = binser.deserialize(grade_history:read())[1]
self.grade = grade_history[1]
self.wins = grade_history[2]
self.plays = grade_history[4]
end
end
if self.entry_pos < 3 then
self.name_entry[self.entry_pos] = self.chars:sub(self.char_pos, self.char_pos)
self.name_entry[self.entry_pos+1] = self.chars:sub(self.char_pos, self.char_pos)
end
self.entry_pos = self.entry_pos + 1
end
elseif e.input == "left" or e.scancode == "left" then
self.delete_confirm = false
self.delete_input_count = 0
self.direction = "left"
elseif e.input == "right" or e.scancode == "right" then
self.delete_confirm = false
self.delete_input_count = 0
self.direction = "right"
elseif e.input == "menu_back" or e.input == "rotate_right" or e.scancode == "delete" or e.scancode == "backspace" then
self.delete_confirm = false
self.delete_input_count = 0
if self.entry_pos == 1 then
scene = TitleScene()
else
self.name_entry[self.entry_pos] = 'A'
self.name_entry[self.entry_pos-1] = 'A'
self.char_pos = 1
self.entry_pos = self.entry_pos - 1
self.grade = 0
end
end
--elseif e.input == "up" or e.scancode == "up" then
-- if self.delete_confirm then
-- self.delete_confirm = false
-- self.delete_input_count = 0
-- end
-- if self.entry_pos == 4 and self.grade > 0 and not self.delete_confirm then
-- self.delete_input_count = self.delete_input_count + 1
-- if self.delete_input_count >= 3 then
-- self.delete_input_count = 0
-- self.delete_confirm = true
-- end
-- end
--elseif e.input == "down" or e.scancode == "down" then
-- if not self.delete_confirm then self.delete_input_count = 0 end
-- if self.entry_pos == 4 and self.delete_confirm then
-- self.delete_input_count = self.delete_input_count + 1
-- if self.delete_input_count >= 3 then
-- love.filesystem.remove(string.lower(self.name_entry[1]..self.name_entry[2]..self.name_entry[3]).."_grade_history.sav")
-- scene = TitleScene()
-- end
-- end
--end
end
function NameEntryScene:onInputRelease(e)
if e.input == "left" or e.scancode == "left" or e.input == "right" or e.scancode == "right" then
self.direction = nil
self.repeat_counter = self.repeat_limit-1
end
end
return NameEntryScene

150
scene/replay.lua Normal file
View File

@@ -0,0 +1,150 @@
require 'funcs'
local ReplaySelectScene = Scene:extend()
ReplaySelectScene.title = "Replay"
local minos = {'R_d', 'O_d', 'Y_d', 'G_d', 'C_d', 'B_d', 'M_d'}
function ReplaySelectScene:new()
self:initList()
PENTO_MODE = false
end
function ReplaySelectScene:initList()
self.replays = {}
replay_list = love.filesystem.getDirectoryItems('saves/replays/')
table.sort(replay_list, function(a,b) return a > b end)
self.replay_text = {}
self.page_flip = 16
self.page = 1
gradeNames = {
"19k", "18k", "17k", "16k", "15k", "14k", "13k", "12k", "11k",
"10k", "9k", "8k", "7k", "6k", "5k", "4k", "3k", "2k", "1k",
"1D", "2D", "3D", "4D", "5D", "6D", "7D", "8D", "9D"
}
for i=1, #replay_list do
replay_found = string.find(replay_list[i], "_replay.sav")
if replay_found ~= nil then
table.insert(self.replays, replay_list[i])
line_components = {}
for str in string.gmatch(replay_list[i], "([^".."_".."]+)") do
table.insert(line_components, str)
end
player_name = line_components[2]
player_grade = gradeNames[tonumber(line_components[3])]
player_score = line_components[4]
if #player_grade == 2 then player_grade = ' '..player_grade end
table.insert(self.replay_text, player_name..' - '..player_grade..' - '..string.format('%6d',player_score))
end
end
self.dialog = false
self.dialog_select = 1
self.replay_select = 1
end
function ReplaySelectScene:render()
mainBackground()
love.graphics.setColor(0.4, 1, 1, 0.5)
love.graphics.rectangle("fill", 0, 20 + 20 * self.replay_select, 640, 22)
drawText('Name - Grade - Score', 40, 20, 1000, "left")
drawText(string.format('Page %s', self.page), 20, 440, 1000, "left")
j = 1
i = 1
while i <= #self.replay_text do
if j > self.page_flip then j = 1
elseif j < 1 then j = self.page_flip
end
if i > (self.page-1) * self.page_flip and i <= self.page * self.page_flip then
drawText(self.replay_text[i], 40, 20 + 20 * j, 1000, "left")
end
j = j + 1
i = i + 1
end
if self.dialog then
love.graphics.setColor(0, 0, 0, 0.5)
love.graphics.rectangle("fill", 23, 43 + 20 * self.replay_select, 65, 40, 5, 5)
love.graphics.setColor(0.3, 0.8, 0.8, 1)
love.graphics.rectangle("fill", 20, 40 + 20 * self.replay_select, 65, 40, 5, 5)
drawText("Play", 30, 40 + 20 * self.replay_select, 1000, "left")
drawText("Delete", 30, 60 + 20 * self.replay_select, 1000, "left")
drawText("o", 20, 40+((self.dialog_select-1) * 20) + (20 * self.replay_select), 1000, "left")
end
if self.replays[1] == nil then
drawText('No replays yet!', 40, 40, 1000, "left")
end
end
function ReplaySelectScene:changeOption(rel)
local len = table.getn(self.replays)
self.replay_select = self.replay_select + rel
if self.replay_select + ((self.page-1) * self.page_flip) > len then
self.page = 1
self.replay_select = 1
elseif self.replay_select < 1 and self.page == 1 then
self.page = 1+(math.floor(len / self.page_flip))
self.replay_select = len - ((self.page-1) * self.page_flip)
end
if self.replay_select > self.page_flip then
self.page = self.page + 1
self.replay_select = 1
elseif self.replay_select < 1 then
self.page = self.page - 1
self.replay_select = self.page_flip
end
end
function ReplaySelectScene:changeDialog(rel)
self.dialog_select = self.dialog_select + rel
if self.dialog_select > 2 then self.dialog_select = 1
elseif self.dialog_select < 1 then self.dialog_select = 2
end
end
function ReplaySelectScene:onInputPress(e)
selected_replay = self.replays[self.replay_select + ((self.page-1) * self.page_flip)]
selected_replay_text = self.replay_text[self.replay_select + ((self.page-1) * self.page_flip)]
if e.input == "menu_decide" or e.input == "rotate_left" or e.scancode == "return" then
if self.replays[1] == nil then
scene = TitleScene()
--if not self.dialog then
-- self.dialog_select = 1
-- self.dialog = true
--elseif self.dialog_select == 2 then
-- love.filesystem.remove(selected_replay)
-- self:initList()
--elseif self.dialog_select == 1 then
-- line_components = {}
-- for str in string.gmatch(selected_replay_text, "([^".."-".."]+)") do
-- table.insert(line_components, str)
-- end
-- player_name = line_components[1]
-- player_grade = string.gsub(line_components[2], " ", "")
-- scene = GameScene(player_name, selected_replay, player_grade)
--end
else
line_components = {}
for str in string.gmatch(selected_replay_text, "([^".."-".."]+)") do
table.insert(line_components, str)
end
player_name = line_components[1]
player_grade = string.gsub(line_components[2], " ", "")
scene = GameScene(player_name, selected_replay, player_grade)
end
elseif e.input == "up" or e.scancode == "up" then
if self.replays[1] == nil then scene = TitleScene() end
if not self.dialog then self:changeOption(-1)
else self:changeDialog(-1) end
elseif e.input == "down" or e.scancode == "down" then
if self.replays[1] == nil then scene = TitleScene() end
if not self.dialog then self:changeOption(1)
else self:changeDialog(1) end
elseif e.input == "menu_back" or e.input == "rotate_right" or e.scancode == "backspace" or e.scancode == "delete" then
if self.replays[1] == nil then scene = TitleScene() end
if not self.dialog then scene = TitleScene()
else self.dialog = false end
end
end
return ReplaySelectScene

158
scene/stick_config.lua Normal file
View File

@@ -0,0 +1,158 @@
local StickConfigScene = Scene:extend()
StickConfigScene.title = "Controller Config"
require 'load.save'
local minos = {'R_d', 'O_d', 'Y_d', 'G_d', 'C_d', 'B_d', 'M_d'}
local configurable_inputs = {
"menu_decide",
"menu_back",
"left",
"right",
"up",
"down",
"rotate_left",
"rotate_left2",
"rotate_right",
"rotate_right2",
}
local input_names = {
menu_decide='Confirm Selection',
menu_back = 'Go Back',
left='Left',
right='Right',
up='Up',
down='Down',
rotate_left='Rotate Counter-clockwise',
rotate_left2='Rotate Counter-clockwise (2)',
rotate_right='Rotate Clockwise',
rotate_right2='Rotate Clockwise (2)'
}
local function newSetInputs()
local set_inputs = {}
for i, input in ipairs(configurable_inputs) do
set_inputs[input] = false
end
return set_inputs
end
function StickConfigScene:new()
self.input_state = 1
self.set_inputs = newSetInputs()
self.new_input = {}
self.axis_timer = 0
end
function StickConfigScene:update()
end
function StickConfigScene:render()
mainBackground()
for i, input in ipairs(configurable_inputs) do
drawText(input_names[input], 40, 50 + i * 20, 200, "left")
if self.set_inputs[input] then
drawText(self.set_inputs[input], 240, 50 + i * 20, 300, "left")
end
end
if self.input_state > table.getn(configurable_inputs) then
drawText("Press enter to confirm, delete/backspace to retry" .. (config.input and ", escape to cancel" or ""), 0, 0, 1000)
else
drawText("Press joystick input for " .. input_names[configurable_inputs[self.input_state]] .. ", tab to skip, escape to cancel", 0, 0, 1000)
end
self.axis_timer = self.axis_timer + 1
end
local function addJoystick(input, name)
if not input[name] then
input[name] = {}
end
end
function StickConfigScene:onInputPress(e)
if e.type == "key" then
-- function keys, escape, and tab are reserved and can't be remapped
if e.scancode == "escape" then
scene = InputConfigScene()
elseif self.input_state > table.getn(configurable_inputs) then
if e.scancode == "return" then
-- save new input, then load next scene
local had_config = config.input ~= nil
if not config.input then config.input = {} end
config.input.joysticks = self.new_input
saveConfig()
scene = had_config and InputConfigScene() or TitleScene()
elseif e.scancode == "delete" or e.scancode == "backspace" then
-- retry
self.input_state = 1
self.set_inputs = newSetInputs()
self.new_input = {}
end
elseif e.scancode == "tab" then
self.set_inputs[configurable_inputs[self.input_state]] = "skipped"
self.input_state = self.input_state + 1
end
elseif string.sub(e.type, 1, 3) == "joy" then
if self.input_state <= table.getn(configurable_inputs) then
if e.type == "joybutton" then
addJoystick(self.new_input, e.name)
if not self.new_input[e.name].buttons then
self.new_input[e.name].buttons = {}
end
if self.new_input[e.name].buttons[e.button] then return end
self.set_inputs[configurable_inputs[self.input_state]] =
"jbtn " ..
e.button ..
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
self.new_input[e.name].buttons[e.button] = configurable_inputs[self.input_state]
self.input_state = self.input_state + 1
elseif e.type == "joyaxis" then
if (e.axis ~= self.last_axis or self.axis_timer > 30) and math.abs(e.value) >= 1 then
addJoystick(self.new_input, e.name)
if not self.new_input[e.name].axes then
self.new_input[e.name].axes = {}
end
if not self.new_input[e.name].axes[e.axis] then
self.new_input[e.name].axes[e.axis] = {}
end
if (
self.new_input[e.name].axes[e.axis][e.value >= 1 and "positive" or "negative"]
) then return end
self.set_inputs[configurable_inputs[self.input_state]] =
"jaxis " ..
(e.value >= 1 and "+" or "-") .. e.axis ..
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
self.new_input[e.name].axes[e.axis][e.value >= 1 and "positive" or "negative"] = configurable_inputs[self.input_state]
self.input_state = self.input_state + 1
self.last_axis = e.axis
self.axis_timer = 0
end
elseif e.type == "joyhat" then
if e.direction ~= "c" then
addJoystick(self.new_input, e.name)
if not self.new_input[e.name].hats then
self.new_input[e.name].hats = {}
end
if not self.new_input[e.name].hats[e.hat] then
self.new_input[e.name].hats[e.hat] = {}
end
if self.new_input[e.name].hats[e.hat][e.direction] then
return
end
self.set_inputs[configurable_inputs[self.input_state]] =
"jhat " ..
e.hat .. " " .. e.direction ..
" " .. string.sub(e.name, 1, 10) .. (string.len(e.name) > 10 and "..." or "")
self.new_input[e.name].hats[e.hat][e.direction] = configurable_inputs[self.input_state]
self.input_state = self.input_state + 1
end
end
end
end
end
return StickConfigScene

106
scene/title.lua Normal file
View File

@@ -0,0 +1,106 @@
local TitleScene = Scene:extend()
require 'load.save'
require 'funcs'
TitleScene.title = "Title"
TitleScene.restart_message = false
local main_menu_screens = {
NameEntryScene,
ReplaySelectScene,
TrainingScene,
InputConfigScene,
FullscreenScene,
MusicToggleScene,
ExitScene,
}
function TitleScene:new()
if sounds['bgm_firsthalf']:isPlaying() or sounds['bgm_secondhalf']:isPlaying() or not config["music"] then
love.audio.stop()
end
self.main_menu_state = 1
PENTO_MODE = false
self.code = {0,0,0,0,0,0,0,0}
end
function TitleScene:update()
end
function TitleScene:drawCredits(top, left)
love.graphics.setColor(0,0,0,0.7)
love.graphics.rectangle("fill", top, left, 335, 345, 10, 10)
drawText("Design & Programming mycophobia ", 6+top, 6+left, 1000, "left")
drawText("Programming Cambridge contributors", 6+top, 21+left, 1000, "left")
drawText("Music Jerry Martin", 6+top, 36+left, 1000, "left")
drawText(" Juraj Stanik", 6+top, 51+left, 1000, "left")
drawText("RNG Consultant colour_thief", 6+top, 66+left, 1000, "left")
drawText("Mac Launcher nightmareci", 6+top, 81+left, 1000, "left")
drawText("People Who Tested switchpalacecorner", 6+top, 106+left, 1000, "left")
drawText("and/or Offered esquatre", 6+top, 121+left, 1000, "left")
drawText("Cool Suggestions Kirby703", 6+top, 136+left, 1000, "left")
drawText(" netdoll", 6+top, 151+left, 1000, "left")
drawText(" lindtobias", 6+top, 166+left, 1000, "left")
drawText(" zaphod77", 6+top, 181+left, 1000, "left")
drawText(" Arch Nemesis", 6+top, 196+left, 1000, "left")
drawText(" dtet_enjoyer", 6+top, 211+left, 1000, "left")
drawText(" woozy", 6+top, 226+left, 1000, "left")
drawText(" AgentBasey", 6+top, 241+left, 1000, "left")
drawText(" Zircean", 6+top, 256+left, 1000, "left")
drawText(" Eden GT", 6+top, 271+left, 1000, "left")
drawText("Special Thanks theabsolute.plus", 6+top, 291+left, 1000, "left")
drawText(" FYAD/Imp Zone Collective", 6+top, 306+left, 1000, "left")
drawText(" All Version 1 Players", 6+top, 321+left, 1000, "left")
end
function TitleScene:render()
mainBackground()
love.graphics.setColor(0,0,0,0.7)
love.graphics.rectangle("fill", 14, 174, 260, 200, 10, 10)
love.graphics.setColor(0,0,0,0.7)
love.graphics.rectangle("fill", 14, 400, 605, 75, 10, 10)
love.graphics.setColor(0.4, 1, 1, 0.5)
love.graphics.rectangle("fill", 20, 198 + 20 * self.main_menu_state, 240, 22)
drawBigText('Tromi', 30, 180, 120, "left")
drawText('version 2', 110, 193, 120, "left")
self:drawCredits(300, 40)
drawText("mycophobia.org", 20, 405, 1000)
drawText("Based on Cambridge - t-sp.in/cambridge", 20, 420, 1000)
drawText("Music for Tromi by Jerry Martin, all rights reserved - jerrymartinmusic.com", 20, 435, 1000)
drawText("Game backgrounds by Pixabay users Joe_hackney, yokim, Favorisxp, Any_Ann, VisualSkyFX ", 20, 450, 1000)
if config["music"] == true then
drawText("On", 230, 320, 1000)
else
drawText("Off", 230, 320, 1000)
end
for i, screen in pairs(main_menu_screens) do
drawText(screen.title, 40, 200 + 20 * i, 1200, "left")
end
if table.concat(self.code, ',') == '1,1,1,1,-1,-1,-1,-1' then PENTO_MODE = true end
if PENTO_MODE then
drawBigText('PENT MODE', 30, 100, 120, "left")
end
end
function TitleScene:changeOption(rel)
local len = table.getn(main_menu_screens)
self.main_menu_state = (self.main_menu_state + len + rel - 1) % len + 1
end
function TitleScene:onInputPress(e)
if e.input == "menu_decide" or e.input == "rotate_left" or e.scancode == "return" then
scene = main_menu_screens[self.main_menu_state]()
elseif e.input == "up" or e.scancode == "up" then
self:changeOption(-1)
elseif e.input == "down" or e.scancode == "down" then
self:changeOption(1)
elseif e.input == "left" or e.scancode == "left" then
table.remove(self.code, 8)
table.insert(self.code, 1, -1)
elseif e.input == "right" or e.scancode == "right" then
table.remove(self.code, 8)
table.insert(self.code, 1, 1)
end
end
return TitleScene

64
scene/training.lua Normal file
View File

@@ -0,0 +1,64 @@
local TrainingScene = Scene:extend()
TrainingScene.title = "Max Gravity Training"
require 'load.save'
function TrainingScene:new()
game_mode = require 'game.gamemode'
if PENTO_MODE then
ruleset = require 'game.rotation_pent'
else
ruleset = require 'game.rotation'
end
self.retry_mode = game_mode
self.retry_ruleset = ruleset
self.secret_inputs = inputs
self.game = game_mode()
self.ruleset = ruleset(self.game)
self.game:initialize(self.ruleset)
self.inputs = {
left=false,
right=false,
up=false,
down=false,
rotate_left=false,
rotate_left2=false,
rotate_right=false,
rotate_right2=false,
rotate_180=false,
hold=false,
}
self.paused = false
end
function TrainingScene:update()
local inputs = {}
for input, value in pairs(self.inputs) do
inputs[input] = value
end
self.game:update(inputs, self.ruleset)
self.game.grid:update()
end
function TrainingScene:render()
self.game:draw(self.paused)
end
function TrainingScene:onInputPress(e)
if (self.game.game_over or self.game.completed) and (e.input == "menu_decide" or e.input == "menu_back" or e.input == "rotate_right") and self.game.game_over_frames > 50 then
scene = TitleScene()
elseif (e.input == "menu_back") then
scene = TitleScene()
elseif e.input and string.sub(e.input, 1, 5) ~= "menu_" then
self.inputs[e.input] = true
end
end
function TrainingScene:onInputRelease(e)
if e.input and string.sub(e.input, 1, 5) ~= "menu_" then
self.inputs[e.input] = false
end
end
return TrainingScene