aboutsummaryrefslogtreecommitdiff
-- mod-version:3

local core = require "core"
local config = require "core.config"
local style = require "core.style"
local common = require "core.common"
local View = require "core.view"
local command = require "core.command"
local keymap = require "core.keymap"

local TetrisView = View:extend()

config.plugins.tetris = common.merge({
  tick = 0.5, -- The amount of time in seconds it takes for a piece to fall one line at 0 score.
  height = 30, -- The amount of cells of height.
  width = 10, -- The amount of cells of width.
  cell_size = 18, -- The size in pixels of each cell.
  cell_padding = 2, -- pixels between each cell
  drop_shadow = true, -- Should cast a drop shadow.
  lock_delay = 3, -- the multiplier for lock delay over a normal tick. set to 0 to disable
  down_amount = 1 -- the amount we move a tetronimo down when you hit the down key; change to math.huge for instant.
}, config.plugins.tetris)

function TetrisView:new(options)
  TetrisView.super.new(self)
  self.cell_size = options.cell_size
  self.cell_padding = options.cell_padding
  self.grid = { x = options.width, y = options.height }
  self.size.x = self.grid.x * (self.cell_size + self.cell_padding) + style.padding.x * 2
  self.cells = { }
  self.score = 0
  self.paused = false
  self.initial_tick = options.tick
  self.lock_delay = options.lock_delay
  self.drop_shadow = options.drop_shadow
  self.tick = self:calculate_tick(self.score)
  self.finished = false
  self.thread = core.add_thread(function()
    while not self.finished do
      self:step()
      core.redraw = true
      coroutine.yield(self.tick)
    end
  end)
  -- easier to specify rotations than to start doing matrix multiplication
  self.tetronimos = {
    {
      color = { common.color "#ff0000" },
      shape = { {
        0,1,1,0,
        0,1,1,0,
        0,0,0,0,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#00ff00" },
      shape = { {
        1,1,0,0,
        1,0,0,0,
        1,0,0,0,
        0,0,0,0
      }, {
        1,1,1,0,
        0,0,1,0,
        0,0,0,0,
        0,0,0,0
      }, {
        0,0,1,0,
        0,0,1,0,
        0,1,1,0,
        0,0,0,0
      }, {
        0,0,0,0,
        1,0,0,0,
        1,1,1,0,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#0000ff" },
      shape = { {
        1,0,0,0,
        1,0,0,0,
        1,1,0,0,
        0,0,0,0
      }, {
        1,1,1,0,
        1,0,0,0,
        0,0,0,0,
        0,0,0,0
      }, {
        0,1,1,0,
        0,0,1,0,
        0,0,1,0,
        0,0,0,0
      }, {
        0,0,0,0,
        0,0,1,0,
        1,1,1,0,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#00ffff" },
      shape = { {
        1,1,0,0,
        0,1,1,0,
        0,0,0,0,
        0,0,0,0
      }, {
        0,0,1,0,
        0,1,1,0,
        0,1,0,0,
        0,0,0,0
      }, {
        0,  0,0,0,
        1,1,0,0,
        0,1,1,0,
        0,0,0,0
      }, {
        0,1,0,0,
        1,1,0,0,
        1,0,0,0,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#ffff00" },
      shape = { {
        0,1,1,0,
        1,1,0,0,
        0,0,0,0,
        0,0,0,0
      }, {
        0,1,0,0,
        0,1,1,0,
        0,0,1,0,
        0,0,0,0
      }, {
        0,0,0,0,
        0,1,1,0,
        1,1,0,0,
        0,0,0,0
      }, {
        1,0,0,0,
        1,1,0,0,
        0,1,0,0,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#ff00ff" },
      shape = { {
        0,1,0,0,
        0,1,0,0,
        0,1,0,0,
        0,1,0,0
      }, {
        0,0,0,0,
        1,1,1,1,
        0,0,0,0,
        0,0,0,0
      }, {
        0,0,1,0,
        0,0,1,0,
        0,0,1,0,
        0,0,1,0
      }, {
        0,0,0,0,
        0,0,0,0,
        1,1,1,1,
        0,0,0,0
      } }
    },
    {
      color = { common.color "#ffffff" },
      shape = { {
        1,1,1,0,
        0,1,0,0,
        0,0,0,0,
        0,0,0,0
      }, {
        0,0,1,0,
        0,1,1,0,
        0,0,1,0,
        0,0,0,0
      }, {
        0,0,0,0,
        0,1,0,0,
        1,1,1,0,
        0,0,0,0
      }, {
        1,0,0,0,
        1,1,0,0,
        1,0,0,0,
        0,0,0,0
      } }
    }
  }
  self.live_piece = nil
  self.hold_piece = nil
end

function TetrisView:calculate_tick(score)
  return self.initial_tick / (math.floor(score / 10) + 1)
end


function TetrisView:does_collide(x, y, tetronimo, rot)
  local shape = tetronimo.shape[rot]
  for i = 0, 3 do
    for j = 0, 3 do
      local ny = y + i
      local nx = x + j
      if (nx >= self.grid.x or ny >= self.grid.y or nx < 0 or ny < 0 or self.cells[self.grid.x * ny + nx + 1]) and shape[i * 4 + j + 1] == 1 then
        return true
      end
    end
  end
  return false
end

function TetrisView:finalize_live_piece()
  assert(self.live_piece)
  local shape = self.live_piece.tetronimo.shape[self.live_piece.rot]
  for i = 0, 3 do
    for j = 0, 3 do
      local ny = self.live_piece.y + i
      local nx = self.live_piece.x + j
      if shape[(i * 4 + j) + 1] == 1 then
        self.cells[ny * self.grid.x + nx + 1] = self.live_piece.idx
      end
    end
  end
  for y = self.live_piece.y, math.min(self.live_piece.y + 4, self.grid.y - 1) do
    local all_present = true
    for x = 0, self.grid.x - 1 do
      if not self.cells[y * self.grid.x + x + 1] then all_present = false end
    end
    if all_present then
      self.score = self.score + 1
      self.tick = self:calculate_tick(self.score)
      for ny = y, 2, -1 do
        for nx = 0, self.grid.x - 1 do
          self.cells[ny * self.grid.x + nx + 1] = self.cells[(ny - 1) * self.grid.x + nx + 1]
        end
      end
    end
  end
  self.live_piece = nil
end


function TetrisView:step()
  if not self.finished and not self.paused then
    if not self.live_piece then
      local idx = self.next_piece or math.floor(math.random() * #self.tetronimos) + 1
      self.live_piece = { tetronimo = self.tetronimos[idx], idx = idx, x = math.floor(self.grid.x / 2), y = 0, rot = 1 }
      self.next_piece = math.floor(math.random() * #self.tetronimos) + 1
      if (self:does_collide(self.live_piece.x, self.live_piece.y + 1, self.live_piece.tetronimo, self.live_piece.rot)) then
        self:finalize_live_piece()
        self.finished = true
      end
    else
      if (self:does_collide(self.live_piece.x, self.live_piece.y + 1, self.live_piece.tetronimo, self.live_piece.rot)) then
        self.live_piece.countup = (self.live_piece.countup or 0) + 1
        if self.live_piece.countup > self.lock_delay then
          self:finalize_live_piece()
        end
      else
        self.live_piece.y = self.live_piece.y + 1
        self.live_piece.countup = 0
      end
    end
  end
end

function TetrisView:draw_tetronimo(posx, posy, tetronimo, rot, color)
  local shape = tetronimo.shape[rot]
  for y = 0, 3 do
    for x = 0, 3 do
      if shape[y * 4 + x + 1] == 1 then
        renderer.draw_rect(posx + x * (self.cell_size + self.cell_padding), posy + y * (self.cell_size + self.cell_padding), self.cell_size, self.cell_size, color or tetronimo.color)
      end
    end
  end
end

function TetrisView:draw()
  self:draw_background(style.background3)
  local lh = style.font:get_height()
  local tx = self.position.x + style.padding.x
  local ty = self.position.y + style.padding.y

  renderer.draw_text(style.font, "Score: " .. self.score, tx, self.position.y + style.padding.y, style.normal)
  local w = renderer.draw_text(style.font, "Next Piece", tx, self.position.y + style.padding.y + lh, style.normal)
  if self.next_piece then
    self:draw_tetronimo(tx, self.position.y + style.padding.y + lh * 2, self.tetronimos[self.next_piece], 1)
  end
  if self.held_piece then
    self:draw_tetronimo(w + style.padding.x, self.position.y + style.padding.y + lh * 2, self.tetronimos[self.held_piece], 1)
  end
  renderer.draw_text(style.font, "Held Piece", w + style.padding.x, self.position.y + style.padding.y + lh, style.normal)
  ty = ty + lh * 2 + (self.cell_size + self.cell_padding) * 4 + style.padding.y

  renderer.draw_rect(tx, ty, (self.cell_size + self.cell_padding) * self.grid.x, (self.cell_size + self.cell_padding) * self.grid.y, style.background)
  for y = 0, self.grid.y - 1 do
    for x = 0, self.grid.x - 1 do
      if self.cells[y * self.grid.x + x + 1] then
        local color = self.tetronimos[self.cells[y * self.grid.x + x + 1]].color
        renderer.draw_rect(tx + x * (self.cell_size + self.cell_padding), ty + y * (self.cell_size + self.cell_padding), self.cell_size, self.cell_size, color)
      end
    end
  end
  if self.live_piece then
    self:draw_tetronimo(tx + self.live_piece.x * (self.cell_size + self.cell_padding), ty + self.live_piece.y * (self.cell_size + self.cell_padding), self.live_piece.tetronimo, self.live_piece.rot)
    if self.drop_shadow then
      local y = self:get_max_drop(math.huge)
      if y ~= self.live_piece.y then
        self:draw_tetronimo(tx + self.live_piece.x * (self.cell_size + self.cell_padding), ty + y * (self.cell_size + self.cell_padding), self.live_piece.tetronimo, self.live_piece.rot, { self.live_piece.tetronimo.color[1], self.live_piece.tetronimo.color[2], self.live_piece.tetronimo.color[3], 50 })
      end
    end
  end
  if self.finished or self.paused then renderer.draw_rect(tx, ty, self.grid.x * (self.cell_size + self.cell_padding), self.grid.y * (self.cell_size + self.cell_padding), { common.color "rgba(255, 255, 255, 0.5)" }) end
  if self.finished then common.draw_text(style.font, style.error, "GAME OVER", "center", tx, ty, self.grid.x * (self.cell_size + self.cell_padding), self.grid.y * (self.cell_size + self.cell_padding)) end
  if self.paused then common.draw_text(style.font, style.warn, "PAUSED", "center", tx, ty, self.grid.x * (self.cell_size + self.cell_padding), self.grid.y * (self.cell_size + self.cell_padding)) end
end

function TetrisView:rotate()
  if self.live_piece and not self.paused then
    local new_rot = (self.live_piece.rot % #self.live_piece.tetronimo.shape) + 1
    if not self:does_collide(self.live_piece.x, self.live_piece.y, self.live_piece.tetronimo, new_rot) then
      self.live_piece.rot = new_rot
    end
  end
end

function TetrisView:hold()
  if self.live_piece and not self.paused then
    if self.held_piece then
      if not self:does_collide(self.live_piece.x, self.live_piece.y, self.tetronimos[self.held_piece], 1) then
        local live_piece = self.live_piece.idx
        self.live_piece = { x = self.live_piece.x, y = self.live_piece.y, rot = 1, idx = self.held_piece, tetronimo = self.tetronimos[self.held_piece] }
        self.held_piece = live_piece
      end
    else
      self.held_piece = self.live_piece.idx
      self.live_piece = nil
    end
  end
end

function TetrisView:get_max_drop(amount)
  if self.live_piece then
    for y = self.live_piece.y, math.min(self.grid.y, self.live_piece.y + amount) do
      if self:does_collide(self.live_piece.x, y + 1, self.live_piece.tetronimo, self.live_piece.rot) then
        return y, true
      end
    end
  end
  return self.live_piece.y + amount, false
end

function TetrisView:drop(amount)
  if self.live_piece and not self.paused then
    local y, collides = self:get_max_drop(amount)
    self.live_piece.y = y
    if collides then
      self:finalize_live_piece()
    end
  end
end

function TetrisView:shift(delta)
  if self.live_piece and not self.paused and not self:does_collide(self.live_piece.x + delta, self.live_piece.y, self.live_piece.tetronimo, self.live_piece.rot) then
    self.live_piece.x = self.live_piece.x + delta
  end
end

command.add(TetrisView, {
  ["tetris:rotate"] = function() core.active_view:rotate() end,
  ["tetris:shift-left"] = function() core.active_view:shift(-1) end,
  ["tetris:shift-right"] = function() core.active_view:shift(1) end,
  ["tetris:drop"] = function() core.active_view:drop(config.plugins.tetris.down_amount) end,
  ["tetris:hard-drop"] = function() core.active_view:drop(math.huge) end,
  ["tetris:hold"] = function() core.active_view:hold() end,
  ["tetris:toggle-pause"] = function() core.active_view.paused = not core.active_view.paused end,
  ["tetris:quit"] = function()
    core.active_view.finished = true
    core.active_view.node:close_view(core.root_view.root_node, core.active_view)
  end
})
command.add(nil, {
  ["tetris:start"] = function()
    local view = TetrisView(config.plugins.tetris)
    local node = core.root_view:get_active_node()
    view.node = node:split("right", view, { x = true }, false)
    core.set_active_view(view)
  end
})

keymap.add {
  ["up"] = "tetris:rotate",
  ["left"] = "tetris:shift-left",
  ["right"] = "tetris:shift-right",
  ["down"] = "tetris:drop",
  ["space"] = "tetris:hard-drop",
  ["tab"] = "tetris:hold",
  ["escape"] = "tetris:quit",
  ["ctrl+e"] = { "tetris:quit", "tetris:start" },
  ["p"] = "tetris:toggle-pause"
}
return { view = TetrisView }