- musicator: generation + ddr operational
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

- TODO: special logic, code cleanup
This commit is contained in:
mr.one
2026-03-21 01:41:30 +01:00
parent 9d6d2c2c6f
commit c41bf23a45
8 changed files with 157 additions and 47 deletions

View File

@@ -1,4 +1,6 @@
{
unpack = unpack or table.unpack
musicator_markov_model = {
model = {
["...|..."] = {
next = {
@@ -640,3 +642,105 @@
},
order = 2
}
local function musicator_unmake_key(k)
local result = {}
for t in string.gmatch(k, "[^|]+") do
result[#result + 1] = t
end
return result
end
local function musicator_count_notes(sequence)
local result = 0
for i,v in ipairs(sequence) do
if not (v == "...") then
result = result + 1
end
end
return result
end
function musicator_generate_sequence(model_data, length)
local order = model_data.order
local model = model_data.model
-- random start key
local model_keys = {}
for k,_ in pairs(model) do
model_keys[#model_keys + 1] = k
end
local start_key = model_keys[math.ceil(math.random() * #model_keys)]
-- sequence starts with the start key
local seq = musicator_unmake_key(start_key)
-- generation loop
while musicator_count_notes(seq) < length do
local current_key = table.concat({unpack(seq, #seq - order + 1, #seq)}, "|")
local chosen = "..."
local key_data = model[current_key]
if key_data then
local r = math.random()
local prob_sum = 0.0
for new_note, new_prob in pairs(key_data.next) do
prob_sum = prob_sum + new_prob
if prob_sum < r then
chosen = new_note
end
end
end
-- print(current_key .. " --> " .. chosen)
seq[#seq+1] = chosen
end
return seq
end
function musicator_row_to_frame(row, bpm, spd)
local frames_per_row = (150 * spd) / bpm
return math.floor(row * frames_per_row)
end
function musicator_note_to_direction(note)
local subnote = note:sub(1,1)
local mapping = {
C="left",
D="up",
E="up",
F="right",
G="right",
A="down"
}
return mapping[subnote] or "up"
end
-- converts generated sequence to pattern that the ddr minigame can consume
function musicator_sequence_to_pattern(sequence, bpm, spd)
local result = {}
for i, note in ipairs(sequence) do
if not (note == "...") then
local at_ms = musicator_row_to_frame(i, bpm, spd)
local direction = musicator_note_to_direction(note)
result[#result + 1] = { frame=at_ms, dir=direction, note=note }
end
end
return result
end
function musicator_generate_pattern(length, bpm, spd)
return musicator_sequence_to_pattern(musicator_generate_sequence(musicator_markov_model, length), bpm, spd)
end

View File

@@ -105,6 +105,15 @@ Songs = {
fps = 60,
end_frame = nil, -- No end frame for random mode
pattern = {} -- Empty, will spawn randomly in game
},
generated = {
name = "Markov Mode",
bpm = 150,
spd = 6,
fps = 60,
end_frame = nil, -- calculated
pattern = {}, -- generated
generated = true
}
}
@@ -162,40 +171,3 @@ Songs.custom_song = {
}, 130)
}
]]
--[[
function generate_sequence(model_data, length)
local order = model.order
local model_data = model_data.model
local model_keys = {}
for k,_ in pairs(model) do
model_keys[#model_keys + 1] = k
end
local start_key = model_keys[math.ceil(math.random() * #model_keys)]
local seq = unmake_key(start_key)
while #seq < length do
local current_key = table.concat({unpack(seq, #seq - order + 1, #seq)}, "|")
local chosen = "..."
local key_data = model[current_key]
if key_data then
local r = math.random()
local prob_sum = 0.0
for new_note, new_prob in pairs(key_data.next) do
prob_sum = prob_sum + new_prob
if prob_sum < r then
chosen = new_note
end
end
end
seq[#seq+1] = chosen
end
return seq
end
]]

View File

@@ -27,7 +27,7 @@ Context = {}
function Context.initial_data()
return {
current_menu_item = 1,
test_mode = false,
test_mode = true,
popup = {
show = false,
content = {}

View File

@@ -433,8 +433,8 @@
-- 000:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000008c10000000008c10000000000000000000000000000000000000000000000000000000000000000004008b50000000000000000001008c10000004008b50000001008c10000000008c1000000e008b30000004008b50000001008c10000000008c10000000008c10000000008c10000000008c10000000008c1000000000000000000000000000000
-- 001:4008b50000000000000000001008c10000004008b50000001008c1000000000000000000e008b30000004008b50000001008c10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007008b50000007008b50000001008c10000007008b50000001008c10000000008c10000007008b50000009008b50000001008c10000009008b50000001008c10000009008b50000009008b50000001008c10000009008b50000001008c1000000
-- 003:4008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000000000000000004008d90000000000000000000000000000000000000000004008d30000000000000000000000000000004008d30000004008d9000000000000000000000000000000000000000000
-- 004:49998d000000e0088b000000b0088b000881e0088b00000040088d000000e0088b000881b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e00889000000
-- 005:400881000000000881000000000881000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000
-- 004:43398d000000e0088b000000b0088b000881e0088b00000040088d000000e0088b000881b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088d000000e0088b000000b0088b000000e0088b00000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e0088900000040088b000000e00889000000b00889000000e00889000000
-- 005:455981000000000881000000000881000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000400881000000000000000000000000000000000000000000400883000000000000000000000000000000000000000000
-- </PATTERNS>
-- <TRACKS>
-- 000:1000012000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff

View File

@@ -90,7 +90,7 @@ end
function MenuWindow.ddr_test()
AudioTestWindow.init()
GameWindow.set_state("minigame_ddr")
MinigameDDRWindow.start("menu", nil)
MinigameDDRWindow.start("menu", "generated")
end
--- Refreshes menu items.

View File

@@ -9,6 +9,7 @@ function MinigameDDRWindow.init_context()
local total_width = (4 * arrow_size) + (3 * arrow_spacing)
local start_x = (Config.screen.width - total_width) / 2
return {
special_type = "normal", -- "normal", "only_special", "only_left", "only_nothing"
bar_fill = 0,
max_fill = 100,
fill_per_hit = 10,
@@ -66,13 +67,23 @@ end
--- @param[opt] params table Optional parameters for minigame configuration.</br>
function MinigameDDRWindow.start(return_window, song_key, params)
MinigameDDRWindow.init(params)
Audio.music_play_activity_work()
Context.minigame_ddr.return_window = return_window or "game"
Context.minigame_ddr.debug_song_key = song_key
if song_key and Songs and Songs[song_key] then
Context.minigame_ddr.current_song = Songs[song_key]
local current_song = Songs[song_key]
Context.minigame_ddr.current_song = current_song
Context.minigame_ddr.use_pattern = true
Context.minigame_ddr.pattern_index = 1
Context.minigame_ddr.debug_status = "Pattern loaded: " .. song_key
if current_song.generated then
local pattern = musicator_generate_pattern(30, current_song.bpm, current_song.spd * 3)
current_song.pattern = pattern
current_song.end_frame = pattern[#pattern].frame
end
else
Context.minigame_ddr.use_pattern = false
if song_key then
@@ -81,12 +92,19 @@ function MinigameDDRWindow.start(return_window, song_key, params)
Context.minigame_ddr.debug_status = "Random mode"
end
end
if not Context.test_mode then
Context.minigame_ddr.debug_status = ""
end
Window.set_current("minigame_ddr")
end
--- Spawns a random arrow.
--- @within MinigameDDRWindow
local function spawn_arrow()
trace("random arrow")
local mg = Context.minigame_ddr
local target = mg.target_arrows[math.random(1, 4)]
table.insert(mg.arrows, {
@@ -99,14 +117,15 @@ end
--- Spawns an arrow in a specific direction.
--- @within MinigameDDRWindow
--- @param direction string The direction of the arrow ("left", "down", "up", "right").
local function spawn_arrow_dir(direction)
local function spawn_arrow_dir(direction, note)
local mg = Context.minigame_ddr
for _, target in ipairs(mg.target_arrows) do
if target.dir == direction then
table.insert(mg.arrows, {
dir = direction,
x = target.x,
y = mg.bar_y + mg.bar_height + 10
y = mg.bar_y + mg.bar_height + 10,
note = note
})
break
end
@@ -165,6 +184,7 @@ function MinigameDDRWindow.update()
mg.win_timer = mg.win_timer - 1
if mg.win_timer == 0 then
mg.special_condition_met = (mg.total_misses == 0)
Audio.music_stop()
Meter.on_minigame_complete()
if mg.on_win then
mg.on_win()
@@ -180,19 +200,23 @@ function MinigameDDRWindow.update()
mg.win_timer = Config.timing.minigame_win_duration
return
end
mg.frame_counter = mg.frame_counter + 1
if mg.use_pattern and mg.current_song and mg.current_song.end_frame then
if mg.frame_counter > mg.current_song.end_frame and #mg.arrows == 0 then
mg.win_timer = Config.timing.minigame_win_duration
return
end
end
if mg.use_pattern and mg.current_song and mg.current_song.pattern then
local pattern = mg.current_song.pattern
while mg.pattern_index <= #pattern do
local spawn_entry = pattern[mg.pattern_index]
if mg.frame_counter >= spawn_entry.frame then
spawn_arrow_dir(spawn_entry.dir)
spawn_arrow_dir(spawn_entry.dir, spawn_entry.note)
mg.pattern_index = mg.pattern_index + 1
else
break
@@ -205,6 +229,8 @@ function MinigameDDRWindow.update()
mg.arrow_spawn_timer = 0
end
end
-- move arrow downwards
local arrows_to_remove = {}
for i, arrow in ipairs(mg.arrows) do
arrow.y = arrow.y + mg.arrow_fall_speed
@@ -217,26 +243,31 @@ function MinigameDDRWindow.update()
mg.total_misses = mg.total_misses + 1
end
end
-- iterate backwards to avoid index shift issues
for i = #arrows_to_remove, 1, -1 do
table.remove(mg.arrows, arrows_to_remove[i])
end
for dir, _ in pairs(mg.input_cooldowns) do
if mg.input_cooldowns[dir] > 0 then
mg.input_cooldowns[dir] = mg.input_cooldowns[dir] - 1
end
end
for dir, _ in pairs(mg.button_pressed_timers) do
if mg.button_pressed_timers[dir] > 0 then
mg.button_pressed_timers[dir] = mg.button_pressed_timers[dir] - 1
end
end
local input_map = {
left = Input.left(),
down = Input.down(),
up = Input.up(),
right = Input.right()
}
for dir, pressed in pairs(input_map) do
if pressed and mg.input_cooldowns[dir] == 0 then
mg.input_cooldowns[dir] = mg.input_cooldown_duration
@@ -244,6 +275,8 @@ function MinigameDDRWindow.update()
local hit = false
for i, arrow in ipairs(mg.arrows) do
if arrow.dir == dir and check_hit(arrow) then
sfx(56, arrow.note)
mg.bar_fill = mg.bar_fill + mg.fill_per_hit
if mg.bar_fill > mg.max_fill then
mg.bar_fill = mg.max_fill