1
0
mirror of https://github.com/Refactorio/RedMew.git synced 2025-03-05 15:05:57 +02:00

Add Tetris (#605)

Somehow nailed everything on the first try. What a pro.
This commit is contained in:
Valansch 2019-01-11 19:25:54 +01:00 committed by Matthew Heguy
parent f09ca708ee
commit 7fa83bae6c
15 changed files with 1711 additions and 378 deletions

View File

@ -57,8 +57,8 @@ global.config = {
paint = {
enabled = true,
},
-- adds a fish market
fish_market = {
-- adds a market
market = {
enabled = true,
currency = currency,
},

View File

@ -41,8 +41,8 @@ end
if config.donator_messages.enabled then
require 'features.donator_messages'
end
if config.fish_market.enabled then
require 'features.fish_market'
if config.market.enabled then
require 'features.market'
end
if config.nuke_control.enabled then
require 'features.nuke_control'

View File

@ -763,7 +763,7 @@ Gui.allow_player_to_toggle_top_element_visibility(main_button_name)
local Public = {}
function Public.show_info(player)
toggle(player)
toggle({player = player})
end
function Public.get_map_name()

View File

@ -129,10 +129,6 @@ local function toggle(event)
direction = 'vertical',
caption = 'Paint Brush'
}
main_frame.add {
type = 'label',
caption = 'Choose a replacement tile for Refined hazard concrete'
}
local tooltip = global.paint_brushes_by_player[event.player_index] or ''

View File

@ -1,3 +1,5 @@
local Module = {}
local Event = require 'utils.event'
local Token = require 'utils.token'
local Gui = require 'utils.gui'
@ -19,6 +21,12 @@ Global.register(
local chest_gui_frame_name = Gui.uid_name()
local chest_content_table_name = Gui.uid_name()
function Module.create_chest(surface, position, storage)
local entity = surface.create_entity{name = 'infinity-chest', position = position, force = 'player'}
chests[entity.unit_number] = {entity = entity, storage = storage}
return entity
end
local function built_entity(event)
local entity = event.created_entity
if not entity or not entity.valid or entity.name ~= 'infinity-chest' then
@ -251,3 +259,5 @@ Gui.on_custom_close(
local market_items = require 'resources.market_items'
table.insert(market_items, {price = 100, name = 'infinity-chest', description = 'Stores unlimited quantity of items for up to 48 different item types'})
return Module

View File

@ -11,7 +11,7 @@ local pairs = pairs
local random = math.random
local format = string.format
local get_random = table.get_random
local currency = global.config.fish_market.currency
local currency = global.config.market.currency
local running_speed_boost_messages = {
'%s found the lost Dragon Scroll and got a lv.1 speed boost!',

View File

@ -50,9 +50,9 @@ local function player_created(event)
p(get_random_weighted(random_messages))
end
if config.show_info_at_start then
if config.show_info_at_start and not _DEBUG then
if Info ~= nil then
Info.show_info({player = player})
Info.show_info(player)
end
end

View File

@ -0,0 +1,437 @@
local Event = require 'utils.event'
local Token = require 'utils.token'
local Task = require 'utils.task'
local Global = require 'utils.global'
local Game = require 'utils.game'
local Debug = require 'utils.debug'
local Map = require 'map_gen.combined.tetris.shape'
local Tetrimino = require 'map_gen.combined.tetris.tetrimino'(Map)
local View = require 'map_gen.combined.tetris.view'
local InfinityChest = require 'features.infinite_storage_chest'
local states = require 'map_gen.combined.tetris.states'
local StateMachine = require 'utils.state_machine'
local tetriminos = {}
local primitives = {
tetri_spawn_y_position = -160,
winner_option_index = 0,
state = states.voting,
next_vote_finished = 305,
points = 0,
down_substate = 0
}
local player_votes = {}
local options = {
{
button = View.button_enum.ccw_button,
action_func_name = 'rotate',
args = {false},
transition = states.moving
},
{
button = View.button_enum.noop_button,
action_func_name = 'noop',
args = {},
transition = states.moving
},
{
button = View.button_enum.cw_button,
action_func_name = 'rotate',
args = {true},
transition = states.moving
},
{
button = View.button_enum.left_button,
action_func_name = 'move',
args = {-1, 0},
transition = states.moving
},
{
button = View.button_enum.down_button,
transition = states.down
},
{
button = View.button_enum.right_button,
action_func_name = 'move',
args = {1, 0},
transition = states.moving
},
{
button = View.button_enum.pause_button,
action_func_name = 'noop',
args = {},
transition = states.pause
}
}
local machine = StateMachine.new(states.voting)
local player_zoom = {}
local player_force = nil
local nauvis = nil
Global.register(
{
tetriminos = tetriminos,
primitives = primitives,
player_votes = player_votes,
player_zoom = player_zoom,
machine = machine
},
function(tbl)
tetriminos = tbl.tetriminos
primitives = tbl.primitives
player_votes = tbl.player_votes
player_zoom = tbl.player_zoom
machine = tbl.machine
end
)
local point_table = {1, 3, 5, 9}
local tetris_tick_duration = 61
global.vote_delay = 10
local function calculate_winner()
if StateMachine.in_state(machine, states.down) then --TODO: Fix
return --Halt vote if in down mode
end
Debug.print('calculating winner')
local vote_sum = {0, 0, 0, 0, 0, 0, 0}
for _, vote in pairs(player_votes) do
vote_sum[vote] = vote_sum[vote] + 1
end
local winners = {}
local max = math.max(vote_sum[1], vote_sum[2], vote_sum[3], vote_sum[4], vote_sum[5], vote_sum[6], vote_sum[7])
for candidate, n_votes in pairs(vote_sum) do
if max == n_votes then
table.insert(winners, candidate)
end
View.set_vote_number(options[candidate].button, n_votes)
end
local winner_option_index = 0
if max > 0 then
winner_option_index = winners[math.random(#winners)]
end
primitives.winner_option_index = winner_option_index
if _DEBUG and (winner_option_index > 0) then
Debug.print('Calculated winner: ' .. View.pretty_names[options[winner_option_index].button])
end
end
local function player_vote(player, option_index)
local old_vote = player_votes[player.index]
if old_vote == option_index then
return
end
local vote_button = nil
local old_vote_button = nil
if option_index then
vote_button = options[option_index].button
end
if old_vote then
old_vote_button = options[old_vote].button
end
player_votes[player.index] = option_index
if _DEBUG then
Debug.print(string.format('%s voted for %s', player.name, View.pretty_names[vote_button]))
end
StateMachine.transition(machine, states.voting)
calculate_winner()
View.set_player_vote(player, vote_button, old_vote_button)
end
for option_index, option in pairs(options) do
View.bind_button(
option.button,
function(player)
player_vote(player, option_index)
end
)
end
local function spawn_new_tetrimino()
table.insert(tetriminos, Tetrimino.new(nauvis, {x = 0, y = primitives.tetri_spawn_y_position}))
end
local function collect_full_row_resources(tetri)
local active_qchunks = Tetrimino.active_qchunks(tetri)
local storage = {}
local full_rows = {}
local rows = {}
local position = tetri.position
local tetri_y = position.y
local surface = tetri.surface
local get_tile = surface.get_tile
local find_entities_filtered = surface.find_entities_filtered
for _, qchunk in pairs(active_qchunks) do
local q_y = qchunk.y
if not rows[q_y] then
rows[q_y] = true
local y = tetri_y + 16 * q_y - 14
local row_full = true
for x = -178, 178, 16 do
local tile = get_tile(x, y)
if tile.valid and tile.name == 'water' then
row_full = false
break
end
end
if row_full then
table.insert(full_rows, q_y)
for _, patch in pairs(find_entities_filtered {type = 'resource', area = {{-178, y}, {162, y + 12}}}) do
local subtotal = storage[patch.name] or 0
storage[patch.name] = subtotal + patch.amount
patch.destroy()
end
end
end
end
if #full_rows > 0 then
local points = point_table[#full_rows]
for resource, amount in pairs(storage) do
storage[resource] = amount * points
if resource == 'crude-oil' then
storage[resource] = nil
if #full_rows == 1 then
return
end
end
end
local x = position.x + active_qchunks[1].x * 16 - 9
local y = tetri_y + active_qchunks[1].y * 16 - 9
local chest = InfinityChest.create_chest(tetri.surface, {x, y}, storage)
chest.minable = false
chest.destructible = false
primitives.points = primitives.points + points * 100
View.set_points(primitives.points)
end
end
local function tetrimino_finished(tetri)
local final_y_position = tetri.position.y
if final_y_position < (primitives.tetri_spawn_y_position + 352) then
primitives.tetri_spawn_y_position = final_y_position - 256
player_force.chart(tetri.surface, {{-192, final_y_position - 352}, {160, final_y_position - 176}})
end
StateMachine.transition(machine, states.voting)
collect_full_row_resources(tetri)
spawn_new_tetrimino()
end
local chart_area =
Token.register(
function(data)
data.force.chart(data.surface, data.area)
end
)
local switch_state =
Token.register(
function(data)
StateMachine.transition(machine, data.state)
end
)
local move_down =
Token.register(
function()
for key, tetri in pairs(tetriminos) do
if not Tetrimino.move(tetri, 0, 1) then
tetrimino_finished(tetri) --If collided with ground fire finished event
tetriminos[key] = nil
end
local pos = tetri.position
Task.set_timeout_in_ticks(
10,
chart_area,
{
force = player_force,
surface = nauvis,
area = {
{pos.x - 32, pos.y - 32},
{pos.x + 64, pos.y + 64}
}
}
)
end
end
)
local function execute_vote_tick()
if game.tick < primitives.next_vote_finished then
return
end
local winner = options[primitives.winner_option_index]
if winner then
StateMachine.transition(machine, winner.transition)
View.set_last_move(winner.button)
else
View.set_last_move(nil)
StateMachine.transition(machine, states.moving)
end
primitives.winner_option_index = 0
for player_index, _ in pairs(player_votes) do -- reset poll
player_votes[player_index] = nil
end
View.reset_poll_buttons()
end
local function execute_winner_action()
for key, tetri in pairs(tetriminos) do --Execute voted action
local winner = options[primitives.winner_option_index]
--Execute voted action
if winner then
local action = Tetrimino[winner.action_func_name]
if action then
action(tetri, winner.args[1], winner.args[2])
end
end
end
Task.set_timeout_in_ticks(16, move_down)
Task.set_timeout_in_ticks(26, switch_state, {state = states.voting})
end
local spawn_new_tetrimino_token = Token.register(spawn_new_tetrimino)
Event.on_init(
function()
player_force = game.forces.player
nauvis = game.surfaces.nauvis
player_force.chart(nauvis, {{-192, -432}, {160, 0}})
Task.set_timeout_in_ticks(30 * tetris_tick_duration - 15, spawn_new_tetrimino_token)
View.enable_vote_buttons(true)
end
)
Event.add(
defines.events.on_tick,
function()
if StateMachine.in_state(machine, states.voting) then
local progress = (primitives.next_vote_finished - game.tick + 1) / global.vote_delay / tetris_tick_duration
if progress >= 0 and progress <= 1 then
View.set_progress(progress)
end
end
end
)
local function execute_down_tick()
local down_state = primitives.down_substate
if down_state > 3 then
primitives.down_substate = 0
StateMachine.transition(machine, states.voting)
return
end
primitives.down_substate = down_state + 1
Task.set_timeout_in_ticks(16, move_down)
end
StateMachine.register_state_tick_callback(machine, states.voting, execute_vote_tick)
StateMachine.register_state_tick_callback(machine, states.down, execute_down_tick)
StateMachine.register_transition_callback(
machine,
states.voting,
states.pause,
function()
View.enable_vote_buttons(true)
game.print('Pausing...')
end
)
StateMachine.register_transition_callback(
machine,
states.pause,
states.voting,
function()
primitives.next_vote_finished = global.vote_delay * tetris_tick_duration + game.tick
game.print('Resuming...')
end
)
StateMachine.register_transition_callback(
machine,
states.moving,
states.voting,
function()
View.enable_vote_buttons(true)
end
)
StateMachine.register_transition_callback(
machine,
states.down,
states.voting,
function()
View.enable_vote_buttons(true)
end
)
StateMachine.register_transition_callback(
machine,
states.voting,
states.down,
function()
primitives.next_vote_finished = (3 + global.vote_delay) * tetris_tick_duration + game.tick
View.enable_vote_buttons(false)
end
)
StateMachine.register_transition_callback(
machine,
states.voting,
states.moving,
function()
View.enable_vote_buttons(false)
primitives.next_vote_finished = global.vote_delay * tetris_tick_duration + game.tick
execute_winner_action()
end
)
Event.on_nth_tick(
tetris_tick_duration,
function()
StateMachine.machine_tick(machine)
end
)
Event.add(
defines.events.on_player_left_game,
function(event)
player_votes[event.player_index] = nil
end
)
Event.add(
defines.events.on_player_created,
function(event)
local player = Game.get_player_by_index(event.player_index)
local position = player.surface.find_non_colliding_position('player', {8, 8}, 3, 1)
if position then
player.teleport(position)
end
end
)
return Map.get_map()

View File

@ -0,0 +1,423 @@
-- Map by grilledham & Jayefuu
-- Set scenario generation cliffs to none.
-- Load blueprint from scenarios\RedMew\map_gen\data\presets\tetris\
-- Obtain items using silent commands from scenarios\RedMew\map_gen\data\presets\tetris\
-- Place the blueprint on the island south of spawn
-- Teleport to centre of island and run the second command in tetris_theme_items_command.txt
-- Excellent tetris themed music generated from midi files, credit to mgabor of miditorio.com
local b = require 'map_gen.shared.builders'
local math = require 'utils.math'
local degrees = math.rad
local ore_seed1 = 7000
local ore_seed2 = ore_seed1 * 2
local noise = require 'map_gen.shared.perlin_noise'.noise
local abs = math.abs
require 'utils.table'
local Random = require 'map_gen.shared.random'
local random = Random.new(ore_seed1, ore_seed2)
local math_random = math.random
local enable_sand_border = false
local function value(base, mult, pow)
return function(x, y)
local d_sq = x * x + y * y
return base + mult * d_sq ^ (pow / 2) -- d ^ pow
end
end
-- Removes vanilla resources when called
local function no_resources(_, _, world, tile)
for _, e in ipairs(
world.surface.find_entities_filtered(
{type = 'resource', area = {{world.x, world.y}, {world.x + 1, world.y + 1}}}
)
) do
e.destroy()
end
return tile
end
local m_t_width = 12 -- map size in number of tiles
local t_width = 16 -- tile width
local t_h_width = t_width / 2
-- https://wiki.factorio.com/Data.raw#tile for the tile types you can send to this function
local function two_tone_square(inner, outer) -- r_tile is a bool flag to show if it should have chance of resources on it
local outer_tile = b.any {b.rectangle(t_width, t_width)}
outer_tile = b.change_tile(outer_tile, true, outer)
local inner_tile = b.any {b.rectangle(t_width - 2, t_width - 2)}
inner_tile = b.change_tile(inner_tile, true, inner)
local land_tile = b.any {inner_tile, outer_tile}
return land_tile
end
local tet_bounds = b.rectangle(t_width * 4)
tet_bounds = b.translate(tet_bounds, t_width, t_width)
local function tetrify(pattern, block)
for r = 1, 4 do
local row = pattern[r]
for c = 1, 4 do
if row[c] == 1 then
row[c] = block
else
row[c] = b.empty_shape()
end
end
end
local grid = b.grid_pattern(pattern, 4, 4, t_width, t_width)
grid = b.translate(grid, -t_width / 2, -t_width / 2)
grid = b.choose(tet_bounds, grid, b.empty_shape)
grid = b.translate(grid, -t_width, -t_width)
return grid
end
local tet_O =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('dirt-7', 'sand-1')
)
local tet_I =
tetrify(
{
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0}
},
two_tone_square('grass-2', 'sand-1')
)
local tet_J =
tetrify(
{
{0, 0, 0, 0},
{0, 0, 1, 0},
{0, 0, 1, 0},
{0, 1, 1, 0}
},
two_tone_square('grass-1', 'sand-1')
)
local tet_L =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 1, 0}
},
two_tone_square('dirt-4', 'sand-1')
)
local tet_S =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{1, 1, 0, 0},
{0, 0, 0, 0}
},
two_tone_square('grass-4', 'sand-1')
)
local tet_Z =
tetrify(
{
{0, 0, 0, 0},
{1, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('grass-3', 'sand-1')
)
local tet_T =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 0, 0},
{1, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('red-desert-2', 'sand-1')
)
local tetriminos = {tet_I, tet_O, tet_T, tet_S, tet_Z, tet_J, tet_L}
local tetriminos_count = #tetriminos
local quarter = math.tau / 4
local p_cols = 1 --m_t_width / 4
local p_rows = 50
local pattern = {}
for _ = 1, p_rows do
local row = {}
table.insert(pattern, row)
for _ = 1, p_cols do
local i = random:next_int(1, tetriminos_count * 1.5)
local shape = tetriminos[i] or b.empty_shape
local angle = random:next_int(0, 3) * quarter
shape = b.rotate(shape, angle)
local x_offset = random:next_int(-10, 8) * t_width
shape = b.translate(shape, x_offset, 0)
table.insert(row, shape)
end
end
local ore_shape = b.rectangle(t_width * 0.8)
local oil_shape = b.throttle_world_xy(ore_shape, 1, 4, 1, 4)
local ores = {
{b.resource(ore_shape, 'iron-ore', value(50, 0.225, 1.15)), 10},
{b.resource(ore_shape, 'copper-ore', value(40, 0.225, 1.15)), 6},
{b.resource(ore_shape, 'stone', value(70, 0.12, 1.075)), 3},
{b.resource(ore_shape, 'coal', value(40, 0.24, 1.075)), 5},
{b.resource(b.scale(ore_shape, 0.5), 'uranium-ore', value(60, 0.09, 1.05)), 2},
{b.resource(oil_shape, 'crude-oil', value(120000, 50, 1.15)), 1},
{b.empty_shape, 100}
}
local total_weights = {}
local t = 0
for _, v in pairs(ores) do
t = t + v[2]
table.insert(total_weights, t)
end
p_cols = 50
p_rows = 50
pattern = {}
for _ = 1, p_rows do
local row = {}
table.insert(pattern, row)
for _ = 1, p_cols do
local i = random:next_int(1, t)
local index = table.binary_search(total_weights, i)
if (index < 0) then
index = bit32.bnot(index)
end
local shape = ores[index][1]
table.insert(row, shape)
end
end
local worm_names = {
'small-worm-turret',
'medium-worm-turret',
'big-worm-turret'
}
local max_worm_chance = 1 / 128
local max_spawner_chance = 1/64
local worm_chance_factor = 1 / (192 * 512)
local function worms(position)
local d = abs(position.y)
local worm_chance = d - 300
if worm_chance > 0 then
worm_chance = worm_chance * worm_chance_factor
worm_chance = math.min(worm_chance, max_worm_chance)
if math_random() < worm_chance then
if d < 600 then
return {name = 'small-worm-turret', position = position}
else
local max_lvl
local min_lvl
if d < 800 then
max_lvl = 2
min_lvl = 1
else
max_lvl = 3
min_lvl = 2
end
local lvl = math_random() ^ (512 / d) * max_lvl
lvl = math.ceil(lvl)
lvl = math.clamp(lvl, min_lvl, 3)
return {name = worm_names[lvl], position = position}
end
end
end
end
local spawner_names = {
'spitter-spawner',
'biter-spawner'
}
local function spawners(position)
local spawner_chance = abs(position.y) - 600
if spawner_chance > 0 then
spawner_chance = spawner_chance * worm_chance_factor
spawner_chance = math.min(spawner_chance, max_spawner_chance)
if math_random() < spawner_chance then
return {name = spawner_names[math_random(1,2)], position = position}
end
end
end
-- Starting area
local start_patch = b.rectangle(t_width * 0.8)
local start_iron_patch =
b.resource(
b.translate(start_patch, -t_width/2, -t_width/2),
'iron-ore',
function()
return 1500
end
)
local start_copper_patch =
b.resource(
b.translate(start_patch, t_width/2, -t_width/2),
'copper-ore',
function()
return 1200
end
)
local start_stone_patch =
b.resource(
b.translate(start_patch, t_width/2, t_width/2),
'stone',
function()
return 900
end
)
local start_coal_patch =
b.resource(
b.translate(start_patch, -t_width/2, t_width/2),
'coal',
function()
return 1350
end
)
local start_resources = b.any({start_iron_patch, start_copper_patch, start_stone_patch, start_coal_patch})
local tet_O_start = b.apply_entity(tet_O, start_resources)
local starting_area = b.any{
b.translate(tet_I,t_width,-t_width*2),
b.translate(tet_O_start,t_width*2,-t_width),
b.translate(tet_T,-t_width,-t_width),
b.translate(tet_Z,-t_width*6,-t_width),
b.translate(tet_L,-t_width*8,-t_width*2)
}
ores = b.grid_pattern_overlap(pattern, p_cols, p_rows, t_width, t_width)
ores = b.translate(ores, t_h_width, t_h_width)
local water_tile = two_tone_square('water', 'deepwater')
local half_sea_width = m_t_width * t_width - t_width
local function sea_bounds(x, y)
return x > -half_sea_width and x < half_sea_width and y < 0
end
local sea = b.single_grid_pattern(water_tile, t_width, t_width)
sea = b.translate(sea, t_h_width, -t_h_width)
sea = b.choose(sea_bounds, sea, b.empty_shape)
local map = b.choose(sea_bounds, starting_area, b.empty_shape)
map = b.if_else(map, sea)
local half_border_width = half_sea_width + t_width
local function border_bounds(x, y)
return x > -half_border_width and x < half_border_width and y < t_width
end
border_bounds = b.subtract(border_bounds, sea_bounds)
local border = b.change_tile(border_bounds, true, 'sand-1')
if enable_sand_border then
map = b.add(map, border)
end
local music_island = b.translate(b.rotate(tet_I,degrees(90)),0, 2*t_width)
map = b.add(map,music_island)
map = b.translate(map, 0, -t_width / 2 + 24)
map = b.apply_effect(map, no_resources)
local bounds = t_width * 2
local Module = {}
local bounds_size = t_width * 4
function Module.spawn_tetri(surface, pos, number)
local tiles = {}
local shape = tetriminos[number]
local offset = math_random(1,1000) * bounds_size
local create_entity = surface.create_entity
local tree = 'tree-0' .. math_random(1,9)
for x = -bounds, bounds do
for y = -bounds, bounds do
local x2, y2 = x + 0.5, y + 0.5
local name = shape(x2, y2)
if name then
local position = {x = pos.x + x, y = pos.y + y}
table.insert(tiles, {name = name, position = position})
if math_random() > 0.8 and (noise(0.02 * x, 0.02 * y,0)) > 0.3 then
create_entity {name = tree, position = position}
else
local n = math_random(1, 599)
if n > 590 then
create_entity{name = 'tree-0' .. n % 10, position = position}
end
end
local ore = ores(x2, y2 - offset, position)
if ore then
ore.position = position
ore.enable_tree_removal = false
create_entity(ore)
end
local worm = worms(position)
if worm then
create_entity(worm)
else
local spawner = spawners(position)
if spawner then
create_entity(spawner)
end
end
end
end
end
surface.set_tiles(tiles)
end
Module.disable = function()
tetriminos = {}
end
Module.get_map = function()
return map
end
return Module

View File

@ -0,0 +1,6 @@
return {
voting = 1,
moving = 2,
down = 3,
pause = 4
}

View File

@ -0,0 +1,387 @@
--- @class This module provides a class that provides Tetrimino object functionallity
local Tetrimino = {}
--- @type LuaTetrimino
-- @field number number identifies the kind of Tetrimino
-- @field collision_box LuaTetriminoCollisionBox @see LuaTetriminoCollisionBox
-- @field position LuaPosition The current position of top left corner of the collision box of the tetrimino.
-- Only x/y divisible by 16 are legal coordinates.
--- @type LuaTetriminoCollisionBox represents the collision box of a tetrimino. Contains a 4x4 2d array.
-- @description If a cell is 1 the quad chunk represented by the array position is occupied. 0 if not
-- @field 1 table of numbers first row
-- @field 2 table of numbers second row
-- @field 3 table of numbers thrid row
-- @field 4 table of numbers forth row
local Token = require 'utils.token'
local Task = require 'utils.task'
local Queue = require 'utils.queue'
local Global = require 'utils.global'
local Game = require 'utils.game'
local insert = table.insert
--- Holds collision boxes for all tetriminos.
-- The index of this table is what defines the tetriminos kind.
-- IE: tetrimino with 1 is the "I" tetrimino
-- @table containing LuaTetriminoCollisionBox
local collision_boxes = {
{
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0}
},
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}
},
{
{0, 0, 0, 0},
{0, 1, 0, 0},
{1, 1, 1, 0},
{0, 0, 0, 0}
},
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{1, 1, 0, 0},
{0, 0, 0, 0}
},
{
{0, 0, 0, 0},
{1, 1, 0, 0},
{0, 1, 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, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 1, 0}
}
}
local worker = nil
local move_queue = Queue.new()
local Map = nil
local sequence = {1, 2, 3, 4, 5, 6, 7, _head = 8}
Global.register(
{
move_queue = move_queue,
sequence = sequence
},
function(tbl)
move_queue = tbl.move_queue
sequence = tbl.sequence
end
)
--- Does this tetrimino collide with anything but itself with a new collision box(rotation) and an x/y offset
-- @param self LuaTetrimino (See @LuaTetrimino) The tetrimino that may collide
-- @param collision_box LuaTetriminoCollisionBox (See @LuaTetriminoCollisionBox) The suggested new collision box of self.
-- @param x_steps number either -1, 0 or 1. Represent going left by 16 tiles, not moving in x direction or going right by 16 tiles
-- @param y_steps number either -1, 0 or 1. Represent going up by 16 tiles, not moving in y direction or going down by 16 tiles
-- @return boolean
local function collides(self, collision_box, x_steps, y_steps)
local old_collision_box = self.collision_box
local position = self.position
local surface = self.surface
local c_x = position.x
local c_y = position.y
for y = 1, 4 do
for x = 1, 4 do
local bit = collision_box[y][x]
if bit == 1 then
local y_offset = y + y_steps
local x_offset = x + x_steps
if
y_offset < 1 or --Cant collide with itself, so continue checking for collision
y_offset > 4 or
x_offset < 1 or
x_offset > 4 or
old_collision_box[y_offset][x_offset] == 0
then --check for collision if not colliding with old self
local x_target = x_offset * 16 + c_x + 2 - 16
local y_target = y_offset * 16 + c_y + 2 - 16
local tile = surface.get_tile {x_target, y_target}
if not tile.valid or tile.name ~= 'water' then
return true
end
end
end
end
end
return false
end
--- Replaces a quad chunk with water and destroys all entities on it
-- @param surface LuaSurface
-- @param x number top left x position of the quad chunk
-- @param y number top left y position of the quad chunk
local function erase_qchunk(surface, x, y)
local new_tiles = {}
for c_x = x + 1, x + 14 do
for c_y = y + 1, y + 14 do
insert(new_tiles, {name = 'water', position = {c_x, c_y}})
end
end
local y_plus15 = y + 15
for c_x = x, x + 15 do
insert(new_tiles, {name = 'deepwater', position = {c_x, y}})
insert(new_tiles, {name = 'deepwater', position = {c_x, y_plus15}})
end
local x_plus15 = x + 15
for c_y = y, y + 14 do
insert(new_tiles, {name = 'deepwater', position = {x, c_y}})
insert(new_tiles, {name = 'deepwater', position = {x_plus15, c_y}})
end
surface.set_tiles(new_tiles)
end
--- Moves a quad chunk (16x16 tiles)
-- @param surface LuaSurface
-- @param x number x position of the quad chunk
-- @param y number y position of the quad chunk
-- @param x_offset number tiles to move in x direction
-- @param y_offset number tiles to move in y direction
local function move_qchunk(surface, x, y, x_offset, y_offset)
local entities = surface.find_entities_filtered {area = {{x, y}, {x + 15, y + 15}}}
local old_tiles = surface.find_tiles_filtered {area = {{x, y}, {x + 16, y + 16}}}
local new_tiles = {}
local player_positions = {}
for index, tile in pairs(old_tiles) do
local old_pos = tile.position
new_tiles[index] = {name = tile.name, position = {x = old_pos.x + x_offset, y = old_pos.y + y_offset}}
end
for index, entity in pairs(entities) do
local old_pos = entity.position
local success,
e =
pcall(
function()
entity.teleport {old_pos.x + x_offset, old_pos.y + y_offset}
end
)
if not success then --I will remove this after the beta :)
game.print('PLEASE TELL VALANSCH OR WE WILL ALL DIE: ')
game.print(entity.name)
game.print(entity.type)
log(entity.name)
log(entity.type)
log('error in create entity ' .. tostring(e))
end
end
surface.set_tiles(new_tiles)
erase_qchunk(surface, x, y)
for player_index, position in pairs(player_positions) do
Game.get_player_by_index(player_index).teleport(position)
end
end
--- Moves the tetrimino in a supplied direction
-- @param self LuaTetrimino
-- @param x_direction number (-1, 0 or 1)
-- @param y_direction number (-1, 0 or 1)
-- @return boolean success
function Tetrimino.move(self, x_direction, y_direction)
local surface = self.surface
local position = self.position
local collision_box = self.collision_box
if collides(self, collision_box, x_direction, y_direction) then
return false
end
local tetri_x = position.x
local tetri_y = position.y
if y_direction == 1 then
for y = 4, 1, -1 do
for x = 1, 4 do
if collision_box[y] and collision_box[y][x] == 1 then
Queue.push(move_queue, {surface = surface, x = tetri_x + (x - 1) * 16, y = tetri_y + (y - 1) * 16, x_offset = 0, y_offset = 16})
end
end
end
elseif x_direction ~= 0 then --east or west
for x = 2.5 + 1.5 * x_direction, 2.5 - 1.5 * x_direction, -x_direction do --go from 1 to 4 or from 4 to 1
for y = 4, 1, -1 do
if collision_box[y] and collision_box[y][x] == 1 then
Queue.push(
move_queue,
{surface = surface, x = tetri_x + (x - 1) * 16, y = tetri_y + (y - 1) * 16, x_offset = x_direction * 16, y_offset = 0}
)
end
end
end
end
position.y = tetri_y + 16 * y_direction
position.x = tetri_x + 16 * x_direction
Task.set_timeout_in_ticks(1, worker)
return true
end
--- Do nothing. Literally.
function Tetrimino.noop()
end
--- Returns a rotated version of a supplied collision box by 90° in mathematically positive direction
-- @param collision_box LuaTetriminoCollisionBox
-- @param[opt=false] reverse boolean rotate in mathematically negative direction?
-- @treturn LuaTetriminoCollisionBox the rotated collision box
local function rotate_collision_box(collision_box, reverse)
local new_collision_box = {{}, {}, {}, {}}
local transformation = {{}, {}, {}, {}}
if reverse then
for y = 1, 4 do
for x = 1, 4 do
new_collision_box[y][x] = collision_box[5 - x][y]
transformation[5 - x][y] = {x = x, y = y}
end
end
else
for y = 1, 4 do
for x = 1, 4 do
new_collision_box[y][x] = collision_box[x][5 - y]
transformation[x][5 - y] = {x = x, y = y}
end
end
end
return new_collision_box, transformation
end
--- Returns the collision box positions of the occupied qchunks in the tetris collision box
-- @param self LuaTetrimino
-- @return table of LuaPosition
function Tetrimino.active_qchunks(self)
local collision_box = self.collision_box
local result = {nil, nil, nil, nil}
for x = 1, 4 do
for y = 1, 4 do
if collision_box[y][x] == 1 then
insert(result, {x = x, y = y})
end
end
end
return result
end
--- Rotates a tetrimino, if it doesnt collide
-- @param self LuaTetrimino
-- @param[opt=false] reverse boolean rotate in mathmatically negative direction?
-- @return boolean success
function Tetrimino.rotate(self, reverse)
local new_collision_box, transformation = rotate_collision_box(self.collision_box, reverse)
if collides(self, new_collision_box, 0, 0) then
return false
end
if self.number == 2 then
game.print("You are a smart motherfucker, that's right.")
end
local old_collision_box = self.collision_box
local surface = self.surface
local find_tiles_filtered = surface.find_tiles_filtered
local find_entities_filtered = surface.find_entities_filtered
local tetri_x = self.position.x
local tetri_y = self.position.y
local insert = insert -- luacheck: ignore 431 (intentional upvalue shadow)
local tiles = {}
local entities = {}
for x = 1, 4 do
for y = 1, 4 do
local target = transformation[y][x]
if
(target.x ~= x or target.y ~= y) and --Do not rotate identity
old_collision_box[y][x] == 1
then --check for existence
local top_left_x = tetri_x + x * 16 - 16
local top_left_y = tetri_y + y * 16 - 16
for _, tile in pairs(find_tiles_filtered {area = {{top_left_x, top_left_y}, {tetri_x + x * 16, tetri_y + y * 16}}}) do
insert(tiles, {name = tile.name, position = {tile.position.x + (target.x - x) * 16, tile.position.y + (target.y - y) * 16}})
end
for _, entity in pairs(find_entities_filtered {area = {{top_left_x, top_left_y}, {tetri_x + x * 16 - 1, tetri_y + y * 16 - 1}}}) do
entity.teleport {entity.position.x + (target.x - x) * 16, entity.position.y + (target.y - y) * 16}
end
if new_collision_box[y][x] == 0 then
erase_qchunk(surface, top_left_x, top_left_y)
end
end
end
end
for _, e in pairs(entities) do
surface.create_entity(e)
end
surface.set_tiles(tiles)
self.collision_box = new_collision_box
return true
end
local function get_next_tetri_number()
local head = sequence._head
if head > 7 then
table.shuffle_table(sequence)
head = 1
sequence._head = 0
end
sequence._head = head + 1
return sequence[head]
end
--- Constructs a new tetri and places it on the map
-- @param surface LuaSurface the surface the tetri will be placed on
-- @param position LuaPosition the position the tetri will be placed at. Legal values for x and y must be divisable by 16
-- @return LuaTetrimino @see LuaTetrimino
function Tetrimino.new(surface, position)
local number = get_next_tetri_number()
local self = {}
self.number = number
self.position = {x = position.x - 32, y = position.y - 32}
self.surface = surface
self.collision_box = collision_boxes[number]
Map.spawn_tetri(surface, position, number)
return self
end
--Works on one quad chunk in the move_queue
worker =
Token.register(
function()
local quad = Queue.pop(move_queue)
if quad then
Task.set_timeout_in_ticks(1, worker)
local surface = quad.surface
local x = quad.x
local y = quad.y
local x_offset = quad.x_offset
local y_offset = quad.y_offset
move_qchunk(surface, x, y, x_offset, y_offset)
end
end
)
--- This module requires a map module as input.
--- @param map_input table containing the function field spawn_tetri(surface, position, number)
return function(map_input)
Map = map_input
return Tetrimino
end

View File

@ -0,0 +1,318 @@
local Module = {}
local Gui = require 'utils.gui'
local Event = require 'utils.event'
local Game = require 'utils.game'
local Global = require 'utils.global'
local main_button_name = Gui.uid_name()
local main_frame_name = Gui.uid_name()
local uids = {
['ccw_button'] = Gui.uid_name(),
['noop_button'] = Gui.uid_name(),
['cw_button'] = Gui.uid_name(),
['left_button'] = Gui.uid_name(),
['down_button'] = Gui.uid_name(),
['right_button'] = Gui.uid_name(),
['pause_button'] = Gui.uid_name(),
['points_label'] = Gui.uid_name()
}
local button_pretty_names = {
[uids.ccw_button] = 'Rotate counter clockwise',
[uids.noop_button] = 'Do nothing',
[uids.cw_button] = 'Rotate clockwise',
[uids.left_button] = 'Move left',
[uids.down_button] = 'Move down',
[uids.right_button] = 'Move right',
[uids.pause_button] = 'Pause'
}
local sprites = {
[uids.ccw_button] = 'utility/reset',
[uids.noop_button] = 'utility/clear',
[uids.cw_button] = 'utility/reset',
[uids.left_button] = 'utility/left_arrow',
[uids.down_button] = 'utility/speed_down',
[uids.right_button] = 'utility/right_arrow',
[uids.pause_button] = 'utility/pause'
}
Module.button_enum = uids
Module.pretty_names = button_pretty_names
local primitives = {
buttons_enabled = false,
points = 0,
progress = 0,
last_move = nil
}
local vote_players = {
[uids.ccw_button] = {},
[uids.noop_button] = {},
[uids.cw_button] = {},
[uids.left_button] = {},
[uids.down_button] = {},
[uids.right_button] = {},
[uids.pause_button] = {}
}
local vote_numbers = {
[uids.ccw_button] = 0,
[uids.noop_button] = 0,
[uids.cw_button] = 0,
[uids.left_button] = 0,
[uids.down_button] = 0,
[uids.right_button] = 0,
[uids.pause_button] = 0
}
Global.register(
{
primitives = primitives,
vote_players = vote_players,
vote_numbers = vote_numbers
},
function(tbl)
primitives = tbl.primitives
vote_players = tbl.vote_players
vote_numbers = tbl.vote_numbers
end
)
local function button_tooltip(button_id)
local tooltip = ''
local non_zero = false
local players = vote_players[button_id]
if not players then
return button_pretty_names[button_id]
end
for _, p_name in pairs(vote_players[button_id]) do
non_zero = true
tooltip = string.format('%s, %s', p_name, tooltip) --If you have a better solution please tell me. Lol.
end
if non_zero then
return string.format('%s: %s', button_pretty_names[button_id], tooltip:sub(1, -3))
end
return button_pretty_names[button_id]
end
local function button_enabled(button_id, player_id)
return (not vote_players[button_id]) or primitives.buttons_enabled and (not vote_players[button_id][player_id])
end
local function add_sprite_button(element, player, name)
return element.add {
type = 'sprite-button',
name = name,
enabled = button_enabled(name, player.index),
tooltip = button_tooltip(name),
number = vote_numbers[name],
sprite = sprites[name]
}
end
local function toggle(player)
if not player then
return
end
local left = player.gui.left
local main_frame = left[main_frame_name]
if main_frame and main_frame.valid then
Gui.destroy(main_frame)
else
main_frame =
left.add {
type = 'frame',
name = main_frame_name,
direction = 'vertical',
caption = 'Tetris'
}
main_frame.style.width = 250
main_frame.add {
type = 'label',
caption = 'Vote on the next move!'
}
local upper_b_f = main_frame.add {type = 'flow', direction = 'horizontal'}
local lower_b_f = main_frame.add {type = 'flow', direction = 'horizontal'}
local vote_buttons = {
[uids.ccw_button] = add_sprite_button(upper_b_f, player, uids.ccw_button),
[uids.noop_button] = add_sprite_button(upper_b_f, player, uids.noop_button),
[uids.cw_button] = add_sprite_button(upper_b_f, player, uids.cw_button),
[uids.left_button] = add_sprite_button(lower_b_f, player, uids.left_button),
[uids.down_button] = add_sprite_button(lower_b_f, player, uids.down_button),
[uids.right_button] = add_sprite_button(lower_b_f, player, uids.right_button),
[uids.pause_button] = add_sprite_button(main_frame, player, uids.pause_button)
}
local progress_bar = main_frame.add {type = 'progressbar', value = primitives.progress}
local points_f = main_frame.add {type = 'flow', direction = 'horizontal'}
points_f.add {
type = 'label',
caption = 'Points: '
}
local points =
points_f.add {
type = 'label',
caption = primitives.points
}
main_frame.add {
type = 'label',
caption = 'Last move:'
}
local last_move_tooltip = 'Do nothing'
local last_move_sprite = nil
local last_move_button = primitives.last_move
if last_move_button then
last_move_tooltip = button_pretty_names[last_move_button]
last_move_sprite = sprites[last_move_button]
end
main_frame.add {
type = 'sprite-button',
enabled = false,
tooltip = last_move_tooltip,
sprite = last_move_sprite
}
local data = {
vote_buttons = vote_buttons,
points = points,
progress_bar = progress_bar
}
Gui.set_data(main_frame, data)
end
end
local function player_joined(event)
local player = Game.get_player_by_index(event.player_index)
if player.gui.top[main_button_name] ~= nil then
return
end
player.gui.top.add {name = main_button_name, type = 'sprite-button', sprite = 'utility/force_editor_icon'}
toggle(Game.get_player_by_index(event.player_index))
end
Gui.on_click(
main_button_name,
function(event)
toggle(event.player)
end
)
function Module.bind_button(button_uid, handler)
Gui.on_click(
button_uid,
function(event)
handler(event.player)
end
)
end
--- Sets the total game points to the given number
-- @param points number
function Module.set_points(points)
primitives.points = points
for _, player in pairs(game.players) do
local mf = player.gui.left[main_frame_name]
if mf then
local data = Gui.get_data(mf)
if data then
data['points'].caption = points
end
end
end
end
--- Sets the number displayed next to a button
-- @param button_id string the buttons uid
-- @param number number then number that will be displayed
function Module.set_vote_number(button_id, number)
vote_numbers[button_id] = number
end
--- Adds a players name to the tooltip of a button and (if applicable) removes the name from another button
-- @description Also disabled the selected button an (if applicable) enables the second
-- @param player LuaPlayer
-- @param vote_button_id string the uid of the button that the players name will be added to
-- @param[opt] old_vote_button_id string the uid of the button that the players name will be removed from
function Module.set_player_vote(player, vote_button_id, old_vote_button_id)
local mf = player.gui.left[main_frame_name]
if mf then
local vote_buttons = Gui.get_data(mf).vote_buttons
if vote_button_id then
vote_buttons[vote_button_id].enabled = false
vote_players[vote_button_id][player.index] = player.name
end
if old_vote_button_id then
vote_buttons[old_vote_button_id].enabled = true
vote_players[old_vote_button_id][player.index] = nil
end
end
for _, p in pairs(game.players) do
toggle(p)
toggle(p)
end
end
--- enables or disables the vote buttons
-- @param enable boolean true if the vote buttons should be enabled, false if not
function Module.enable_vote_buttons(enable)
primitives.buttons_enabled = enable
for _, player in pairs(game.players) do
local mf = player.gui.left[main_frame_name]
if mf then
local data = Gui.get_data(mf)
if data then
local buttons = data.vote_buttons
for _, button in pairs(buttons) do
button.enabled = button_enabled(button.name, player.index)
end
end
end
end
end
--- Sets the last move
-- @param button_id string the button id of the last move
function Module.set_last_move(button_id)
primitives.last_move = button_id
end
--- Resets all poll buttons back to default
function Module.reset_poll_buttons()
for key, _ in pairs(vote_players) do
vote_players[key] = {}
vote_numbers[key] = 0
end
for _, player in pairs(game.players) do
toggle(player)
toggle(player)
end
end
--- Sets progressbar denoting the time left until the current vote is finished
-- @param progress number between 0 and 1
function Module.set_progress(progress)
primitives.progress = progress
for _, player in pairs(game.players) do
local mf = player.gui.left[main_frame_name]
if mf then
local data = Gui.get_data(mf)
if data then
data.progress_bar.value = progress
end
end
end
end
Event.add(defines.events.on_player_joined_game, player_joined)
return Module

View File

@ -1,364 +1 @@
-- Map by grilledham & Jayefuu
-- Set scenario generation cliffs to none.
-- Load blueprint from scenarios\RedMew\map_gen\data\presets\tetris\
-- Obtain items using silent commands from scenarios\RedMew\map_gen\data\presets\tetris\
-- Place the blueprint on the island south of spawn
-- Teleport to centre of island and run the second command in tetris_theme_items_command.txt
-- Excellent tetris themed music generated from midi files, credit to mgabor of miditorio.com
local b = require 'map_gen.shared.builders'
local math = require 'utils.math'
local degrees = math.rad
local ore_seed1 = 7000
local ore_seed2 = ore_seed1 * 2
local Random = require 'map_gen.shared.random'
local random = Random.new(ore_seed1, ore_seed2)
local function value(base, mult, pow)
return function(x, y)
local d_sq = x * x + y * y
return base + mult * d_sq ^ (pow / 2) -- d ^ pow
end
end
-- Removes vanilla resources when called
local function no_resources(x, y, world, tile)
for _, e in ipairs(
world.surface.find_entities_filtered(
{type = 'resource', area = {{world.x, world.y}, {world.x + 1, world.y + 1}}}
)
) do
e.destroy()
end
return tile
end
local names = {
'biter-spawner',
'spitter-spawner'
}
-- removes spawners when called
local function no_spawners(x, y, world, tile)
for _, e in ipairs(
world.surface.find_entities_filtered(
{force = 'enemy', name = names, position = {world.x, world.y}}
)
) do
e.destroy()
end
return tile
end
local m_t_width = 12 -- map size in number of tiles
local t_width = 16 -- tile width
local t_h_width = t_width / 2
-- https://wiki.factorio.com/Data.raw#tile for the tile types you can send to this function
local function two_tone_square(inner, outer) -- r_tile is a bool flag to show if it should have chance of resources on it
local outer_tile = b.any {b.rectangle(t_width, t_width)}
outer_tile = b.change_tile(outer_tile, true, outer)
local inner_tile = b.any {b.rectangle(t_width - 2, t_width - 2)}
inner_tile = b.change_tile(inner_tile, true, inner)
local land_tile = b.any {inner_tile, outer_tile}
return land_tile
end
local tet_bounds = b.rectangle(t_width * 4)
tet_bounds = b.translate(tet_bounds, t_width, t_width)
local function tetrify(pattern, block)
for r = 1, 4 do
local row = pattern[r]
for c = 1, 4 do
if row[c] == 1 then
row[c] = block
else
row[c] = b.empty_shape()
end
end
end
local grid = b.grid_pattern(pattern, 4, 4, t_width, t_width)
grid = b.translate(grid, -t_width / 2, -t_width / 2)
grid = b.choose(tet_bounds, grid, b.empty_shape)
grid = b.translate(grid, -t_width, -t_width)
--grid = b.translate(grid, -t_width, t_width * 2)
return grid
end
local tet_O =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('dirt-7', 'sand-1')
)
local tet_I =
tetrify(
{
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0}
},
two_tone_square('grass-2', 'sand-1')
)
local tet_J =
tetrify(
{
{0, 0, 0, 0},
{0, 0, 1, 0},
{0, 0, 1, 0},
{0, 1, 1, 0}
},
two_tone_square('grass-1', 'sand-1')
)
local tet_L =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 0, 0},
{0, 1, 0, 0},
{0, 1, 1, 0}
},
two_tone_square('dirt-4', 'sand-1')
)
local tet_S =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 1, 0},
{1, 1, 0, 0},
{0, 0, 0, 0}
},
two_tone_square('grass-4', 'sand-1')
)
local tet_Z =
tetrify(
{
{0, 0, 0, 0},
{1, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('grass-3', 'sand-1')
)
local tet_T =
tetrify(
{
{0, 0, 0, 0},
{0, 1, 0, 0},
{1, 1, 1, 0},
{0, 0, 0, 0}
},
two_tone_square('red-desert-2', 'sand-1')
)
local tetriminos = {tet_I, tet_O, tet_T, tet_S, tet_Z, tet_J, tet_L}
local tetriminos_count = #tetriminos
local quarter = math.tau / 4
local p_cols = 1 --m_t_width / 4
local p_rows = 50
local pattern = {}
for _ = 1, p_rows do
local row = {}
table.insert(pattern, row)
for _ = 1, p_cols do
--m_t_width
--t_width
-- map_width = m_t_width*t_width
local i = random:next_int(1, tetriminos_count * 1.5)
local shape = tetriminos[i] or b.empty_shape
local angle = random:next_int(0, 3) * quarter
shape = b.rotate(shape, angle)
--local y_offset = random:next_int(-2, 2) * t_width
local x_offset = random:next_int(-10, 8) * t_width
shape = b.translate(shape, x_offset, 0)
table.insert(row, shape)
end
end
local tetriminos_shape = b.grid_pattern(pattern, p_cols, p_rows, t_width * 24, t_width * 4)
tetriminos_shape = b.translate(tetriminos_shape, t_width, -t_width)
local ore_shape = b.rectangle(t_width * 0.8)
local oil_shape = b.throttle_world_xy(ore_shape, 1, 4, 1, 4)
local ores = {
{b.resource(ore_shape, 'iron-ore', value(250, 0.75, 1.15)), 10},
{b.resource(ore_shape, 'copper-ore', value(200, 0.75, 1.15)), 6},
{b.resource(ore_shape, 'stone', value(350, 0.4, 1.075)), 3},
{b.resource(ore_shape, 'coal', value(200, 0.8, 1.075)), 5},
{b.resource(b.scale(ore_shape, 0.5), 'uranium-ore', value(300, 0.3, 1.05)), 2},
{b.resource(oil_shape, 'crude-oil', value(120000, 50, 1.15)), 1},
{b.empty_shape, 100}
}
local total_weights = {}
local t = 0
for _, v in pairs(ores) do
t = t + v[2]
table.insert(total_weights, t)
end
p_cols = 50
p_rows = 50
pattern = {}
for _ = 1, p_rows do
local row = {}
table.insert(pattern, row)
for _ = 1, p_cols do
local i = random:next_int(1, t)
local index = table.binary_search(total_weights, i)
if (index < 0) then
index = bit32.bnot(index)
end
local shape = ores[index][1]
table.insert(row, shape)
end
end
local worm_names = {
'small-worm-turret',
'medium-worm-turret',
'big-worm-turret'
}
local max_worm_chance = 1 / 128
local worm_chance_factor = 1 / (192 * 512)
local function worms(_, _, world)
local wx, wy = world.x, world.y
local d = math.sqrt(wx * wx + wy * wy)
local worm_chance = d - 128
if worm_chance > 0 then
worm_chance = worm_chance * worm_chance_factor
worm_chance = math.min(worm_chance, max_worm_chance)
if math.random() < worm_chance then
if d < 256 then
return {name = 'small-worm-turret'}
else
local max_lvl
local min_lvl
if d < 512 then
max_lvl = 2
min_lvl = 1
else
max_lvl = 3
min_lvl = 2
end
local lvl = math.random() ^ (512 / d) * max_lvl
lvl = math.ceil(lvl)
lvl = math.clamp(lvl, min_lvl, 3)
return {name = worm_names[lvl]}
end
end
end
end
-- Starting area
local start_patch = b.rectangle(t_width * 0.8)
local start_iron_patch =
b.resource(
b.translate(start_patch, -t_width/2, -t_width/2),
'iron-ore',
function()
return 1500
end
)
local start_copper_patch =
b.resource(
b.translate(start_patch, t_width/2, -t_width/2),
'copper-ore',
function()
return 1200
end
)
local start_stone_patch =
b.resource(
b.translate(start_patch, t_width/2, t_width/2),
'stone',
function()
return 900
end
)
local start_coal_patch =
b.resource(
b.translate(start_patch, -t_width/2, t_width/2),
'coal',
function()
return 1350
end
)
local start_resources = b.any({start_iron_patch, start_copper_patch, start_stone_patch, start_coal_patch})
local tet_O_start = b.apply_entity(tet_O, start_resources)
local starting_area = b.any{
b.translate(tet_I,t_width,-t_width*2),
b.translate(tet_O_start,t_width*2,-t_width),
b.translate(tet_T,-t_width,-t_width),
b.translate(tet_Z,-t_width*6,-t_width),
b.translate(tet_L,-t_width*8,-t_width*2)
}
tetriminos_shape = b.any{tetriminos_shape, starting_area}
ores = b.grid_pattern_overlap(pattern, p_cols, p_rows, t_width, t_width)
ores = b.translate(ores, t_h_width, t_h_width)
tetriminos_shape = b.apply_entity(tetriminos_shape, ores) -- add ores to tetriminoes
tetriminos_shape = b.apply_effect(tetriminos_shape, no_spawners) -- remove spawners to help pathing
tetriminos_shape = b.apply_entity(tetriminos_shape, worms) -- add worms
local water_tile = two_tone_square('water', 'deepwater')
local half_sea_width = m_t_width * t_width - t_width
local function sea_bounds(x, y)
return x > -half_sea_width and x < half_sea_width and y < 0
end
local sea = b.single_grid_pattern(water_tile, t_width, t_width)
sea = b.translate(sea, t_h_width, -t_h_width)
sea = b.choose(sea_bounds, sea, b.empty_shape)
local map = b.choose(sea_bounds, tetriminos_shape, b.empty_shape)
map = b.if_else(map, sea)
local half_border_width = half_sea_width + t_width
local function border_bounds(x, y)
return x > -half_border_width and x < half_border_width and y < t_width
end
border_bounds = b.subtract(border_bounds, sea_bounds)
local border = b.change_tile(border_bounds, true, 'sand-1')
map = b.add(map, border)
local music_island = b.translate(b.rotate(tet_I,degrees(90)),0, 2*t_width)
map = b.add(map,music_island)
map = b.translate(map, 0, -t_width / 2)
map = b.apply_effect(map, no_resources)
return map
return require 'map_gen.combined.tetris.control'

View File

@ -25,8 +25,9 @@ function Queue.pop(queue)
local element = queue[index]
queue[index] = nil
queue._tail = index - 1
if element then
queue._tail = index - 1
end
return element
end

118
utils/state_machine.lua Normal file
View File

@ -0,0 +1,118 @@
--- This module provides a classical mealy/moore state machine.
-- Each machine in constructed by calling new()
-- States and Transitions are lazily added to the machine as transition handlers and state tick handlers are registered.
-- However the state machine must be fully defined after init is done. Dynamic machine changes are currently unsupported
-- An example usage can be found here: map_gen\combined\tetris\control.lua
local Module = {}
local Debug = require 'utils.debug'
local in_state_callbacks = {}
local transaction_callbacks = {}
local max_stack_depth = 20
local machine_count = 0
--- Transitions the supplied machine into a given state and executes all transaction_callbacks
-- @param self StateMachine
-- @param new_state number/string The new state to transition to
function Module.transition(self, new_state)
Debug.print(string.format('Transitioning from state %d to state %d.', self.state, new_state))
local old_state = self.state
local stack_depth = self.stack_depth
self.stack_depth = stack_depth + 1
if stack_depth > max_stack_depth then
if _DEBUG then
error('[WARNING] Stack overflow at:' .. debug.traceback())
else
log('[WARNING] Stack overflow at:' .. debug.traceback())
end
end
local exit_callbacks = transaction_callbacks[self.id][old_state]
if exit_callbacks then
local entry_callbacks = exit_callbacks[new_state]
if entry_callbacks then
for i = 1, #entry_callbacks do
local callback = entry_callbacks[i]
if callback then
callback()
end
end
end
end
self.state = new_state
end
--- Is this machine in this state?
-- @param self StateMachine
-- @param state number/string
-- @return boolean
function Module.in_state(self, state)
return self.state == state
end
--- Invoke a machine tick. Will execute all in_state_callbacks of the given machine
-- @param self StateMachine the machine, whose handlers will be invoked
function Module.machine_tick(self)
local callbacks = in_state_callbacks[self.id][self.state]
if callbacks then
for i=1, #callbacks do
local callback = callbacks[i]
if callback then
callback()
end
end
end
self.stack_depth = 0
end
--- Register a handler that will be invoked by StateMachine.machine_tick
-- You may register multiple handlers for the same transition
-- NOTICE: This function will invoke an error if called after init. Dynamic machine changes are currently unsupported
-- @param self StateMachine the machine
-- @param state number/string The state, that the machine will be in, when callback is invoked
-- @param callback function
function Module.register_state_tick_callback(self, state, callback)
if game then
error('StateMachine.register_state_tick_callback after on_init() is unsupported due to desyncs.', 2)
end
in_state_callbacks[self.id][state] = in_state_callbacks[self.id][state] or {}
table.insert(in_state_callbacks[self.id][state], callback)
end
--- Register a handler that will be invoked by StateMachine.transition
-- You may register multiple handlers for the same transition
-- NOTICE: This function will invoke an error if called after init. Dynamic machine changes are currently unsupported
-- @param self StateMachine the machine
-- @param state number/string exiting state
-- @param state number/string entering state
-- @param callback function
function Module.register_transition_callback(self, old, new, callback)
if game then
error('StateMachine.register_transition after on_init() is unsupported due to desyncs.', 2)
end
transaction_callbacks[self.id][old] = transaction_callbacks[self.id][old] or {}
transaction_callbacks[self.id][old][new] = transaction_callbacks[self.id][old][new] or {}
table.insert(transaction_callbacks[self.id][old][new], callback)
end
--- Constructs a new state machine
-- @param init_state number/string The starting state of the machine
-- @return StateMachine The constructed state machine object
function Module.new(init_state)
if game then
error('StateMachine.register_transition after on_init() is unsupported due to desyncs.', 2)
end
machine_count = machine_count + 1
in_state_callbacks[machine_count] = {}
transaction_callbacks[machine_count] = {}
return {
state = init_state,
stack_depth = 0,
id = machine_count,
}
end
return Module