Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- GRID_SIZE=8
- CELL_SIZE=8
- GRID_X=40
- GRID_Y=56
- PREVIEW_BOX=32
- PREVIEW_GAP=6
- RIGHT_MARGIN=8
- RIGHT_PANEL_X=240-RIGHT_MARGIN-PREVIEW_BOX
- RIGHT_PANEL_TOP=(136-(3*PREVIEW_BOX+2*PREVIEW_GAP))//2+5
- SCORE_Y=18
- SCORE_WIDTH=120
- SCORE_HEIGHT=26
- SCORE_X=8
- PREVIEW_CELL_CAP=CELL_SIZE
- PREVIEW_MIN_CELL=4
- SHAKE_MULT=0.15
- SHAKE_MAX=0.8
- FLASH_FRAMES=6
- WAVE_TIME=10
- LINE_WIDTH_MAX=6
- ENABLE_SCREEN_FLASH=false
- BG_COLORS={{r=48,g=0,b=73},{r=74,g=0,b=114},{r=213,g=132,b=231},{r=0,g=131,b=191},{r=0,g=105,b=92}}
- grace_max=3
- grace_remaining=0
- warning_active=false
- warning_beep_timer=0
- flash_color=12
- heart_beat_timer=0
- turns_since_line_clear=0
- line_guarantee_active=false
- grid={}
- score=0
- best_score=0
- combo=0
- next_shapes={}
- dragging=nil
- mouse_x=0
- mouse_y=0
- game_over=false
- clearing_cells={}
- particles={}
- shake=0
- flash=0
- score_popups={}
- wave_effect=0
- rainbow_offset=0
- line_clear_effects={}
- bg_time=0
- sound_events={}
- music_time=0
- music_playing=true
- last_beat=0
- beat_duration=240
- title_glow=0
- title_float=0
- shapes={{{1}},{{1,1}},{{1},{1}},{{1,1,1}},{{1},{1},{1}},{{1,0},{1,0},{1,1}},{{0,1},{0,1},{1,1}},{{1,1,1},{0,1,0}},{{1,1},{1,1}},{{0,1,0},{1,1,1}},{{1,1,0},{0,1,1}},{{0,1,1},{1,1,0}},{{1,1,1,1}},{{1},{1},{1},{1}},{{1,0,0},{1,1,1}},{{0,0,1},{1,1,1}},{{1,0},{1,0},{1,0},{1,1}},{{0,1},{0,1},{0,1},{1,1}},{{1,0,1},{1,1,1}},{{1,0,0},{1,1,0},{0,1,1}},{{1,1,1},{1,1,1}},{{1,1,1},{1,1,1},{1,1,1}},{{1,1},{1,1},{1,0}},{{0,1,1},{1,1,0}},{{1,1,0},{0,1,1}},{{1,1,1},{0,1,0}}}
- colors={2,3,4,5,6,7,8,9,10,11}
- USE_WEIGHTED_BAG=true
- WB_R_TRIES=3
- WB_P_FAIL=0.15
- WB_PITY_CAP=2
- WB_WINDOW=20
- WB_MAX_MICRO_IN_WIN=4
- WB_MAX_SMALL_IN_WIN=10
- WB_MAX_MEDIUM_IN_WIN=9
- WB_MAX_LARGE_IN_WIN=6
- WB_MAX_AWK_IN_WIN=4
- WB_W_MICRO=1.00
- WB_W_SMALL=0.85
- WB_W_MEDIUM=0.60
- WB_W_LARGE=0.35
- WB_W_AWK=0.20
- WB_CRAMPED_LARGEST_PATCH=5
- WB_CRAMPED_HOLES=3
- wb_recent_classes={}
- wb_consec_dead=0
- function sfx_pick()
- sfx(0, 28, 6, 0, 8, 0)
- end
- function sfx_place()
- playBlockPlace()
- end
- function sfx_invalid()
- sfx(0, 18, 10, 0, 8, 0)
- end
- function sfx_clear(cellcount)
- if cellcount > 8 then
- playCombo() -- Big combo sound
- else
- playLineClear() -- Regular line clear
- end
- end
- function sfx_streak_up(level)
- if level <= 2 then
- sfx(0, 24, 6, 0, 8, 0) -- Low C
- sfx(1, 28, 6, 1, 7, 0) -- Low E
- elseif level <= 4 then
- sfx(0, 24, 6, 0, 9, 0) -- Low C
- sfx(1, 28, 6, 1, 8, 0) -- Low E
- sfx(2, 31, 6, 2, 7, 0) -- Low G
- else
- sfx(0, 24, 6, 0, 10, 0) -- Low C
- sfx(1, 28, 6, 1, 9, 0) -- Low E
- sfx(2, 31, 6, 2, 8, 0) -- Low G
- sfx(3, 36, 8, 3, 10, 0) -- Mid C (still reasonable)
- end
- end
- function sfx_warning_tick()
- sfx(0, 30, 4, 0, 6, 0) -- Low gentle beep
- end
- function sfx_streak_break()
- sfx(0, 32, 8, 0, 10, 0) -- Low notes only
- sfx(1, 28, 8, 1, 9, 0)
- sfx(2, 24, 10, 2, 8, 0)
- sfx(3, 20, 12, 3, 10, 0) -- Very low final note
- end
- function sfx_game_over()
- sfx(0, 31, 15, 0, 10, 0) -- Low G
- sfx(1, 28, 15, 1, 9, 0) -- Low E
- sfx(2, 24, 15, 2, 8, 0) -- Low C
- sfx(3, 18, 20, 3, 10, 0) -- Very low ending
- end
- function sfx_restart()
- sfx(0, 24, 8, 0, 8, 0) -- Low C
- sfx(1, 28, 8, 1, 7, 0) -- Low E
- sfx(2, 31, 8, 2, 7, 0) -- Low G
- sfx(3, 36, 10, 3, 8, 0) -- Mid C (gentle)
- end
- function setupMusic()
- end
- function update_music()
- if not music_playing then return end
- end
- function playBlockPlace()
- sfx(0, 24, 8, 0, 10, 0) -- Deep woody sound
- end
- function playLineClear()
- sfx(1, 32, 12, 1, 8, 0) -- Gentle celebration
- end
- function playCombo()
- sfx(2, 40, 15, 2, 6, 0) -- Happy but reasonable pitch
- end
- local function update_warning_audio()
- if warning_active then
- warning_beep_timer = warning_beep_timer - 1
- local period = math.max(20, 60 - combo*8) -- Less frequent beeping
- if warning_beep_timer<=0 then
- sfx_warning_tick()
- warning_beep_timer = period
- end
- else
- warning_beep_timer = 0
- end
- end
- function lerp_color(c1, c2, t)
- return {
- r=math.floor(c1.r + (c2.r - c1.r) * t),
- g=math.floor(c1.g + (c2.g - c1.g) * t),
- b=math.floor(c1.b + (c2.b - c1.b) * t)
- }
- end
- function rgb_to_tic_color(color)
- local r, g, b = color.r, color.g, color.b
- if r < 50 and g < 50 and b > 50 then return 1 end
- if r < 100 and g < 50 and b > 80 then return 2 end
- if r > 150 and g > 100 and b > 200 then return 14 end
- if r < 50 and g > 100 and b > 150 then return 9 end
- if r < 50 and g > 80 and b > 80 then return 8 end
- return 1
- end
- function draw_animated_background()
- bg_time=bg_time+0.02
- for y=0,135 do
- local w1=math.sin(y*0.05+bg_time)*0.5+0.5 local w2=math.sin(y*0.03+bg_time*1.3)*0.5+0.5 local w3=math.sin(y*0.08+bg_time*0.7)*0.5+0.5
- local t1=(w1*3)%1 local t2=(w2*2)%1 local t3=(w3*4)%1
- local i1=math.floor(w1*(#BG_COLORS-1))+1 local i2=(i1%#BG_COLORS)+1 local i3=math.floor(w3*(#BG_COLORS-1))+1
- local c1=lerp_color(BG_COLORS[i1],BG_COLORS[i2],t1) local c2=lerp_color(BG_COLORS[i3],BG_COLORS[1],t2)
- line(0,y,239,y,rgb_to_tic_color(lerp_color(c1,c2,t3*0.3)))
- end
- for i=1,12 do
- local x=(math.sin(bg_time*0.4+i*0.7)*100+120)%240
- local y=(math.cos(bg_time*0.3+i*1.2)*50+68)%136
- local size=math.sin(bg_time*0.8+i)*1.5+2
- local pulse=math.sin(bg_time*2+i)*0.5+0.5
- local color=pulse>0.7 and 12 or 14
- circb(x,y,size,color)
- end
- end
- local function streak_multiplier()
- return 1 + 0.5 * combo
- end
- local function streak_ui_color(level)
- if level<=0 then return 12 end
- if level<=2 then return 11 end
- if level<=4 then return 2 end
- return get_rainbow_color(rainbow_offset)
- end
- local function enter_warning()
- if warning_active then return end
- warning_active=true
- warning_beep_timer=1
- for i=1,20 do
- local gx=GRID_X-6+math.random()*((GRID_SIZE*9)+12)
- local gy=GRID_Y-6+math.random()*((GRID_SIZE*9)+12)
- table.insert(particles,{x=gx,y=gy,vx=(math.random()-0.5)*0.5,vy=(math.random()-0.5)*0.5,life=20,color=6,warning=true})
- end
- end
- local function exit_warning()
- warning_active=false
- end
- local function streak_break()
- combo=0
- grace_remaining=0
- exit_warning()
- flash=FLASH_FRAMES
- flash_color=6 -- red flash
- sfx_streak_break()
- end
- function draw_heart(x, y, size, filled, color, outline_color, beating)
- local s = size
- local beat_offset = 0
- if beating then
- beat_offset = math.sin(time() * 0.15) * 0.5 + 0.5
- s = s + beat_offset
- end
- local function draw_shape(px, py, psize, pfilled, pcolor)
- if pfilled then
- for dy = -psize, psize do
- for dx = -psize, psize do
- local dist = math.sqrt(dx*dx + dy*dy)
- local angle = math.atan2(dy, dx)
- local heart_dist = psize * (1 - math.sin(angle + math.pi/2) * 0.3)
- if dy < 0 then
- if (dx < 0 and (dx+psize/2)*(dx+psize/2) + dy*dy < (psize/2)*(psize/2)) or
- (dx > 0 and (dx-psize/2)*(dx-psize/2) + dy*dy < (psize/2)*(psize/2)) then
- pix(px + dx, py + dy, pcolor)
- end
- else
- if math.abs(dx) < psize - dy then
- pix(px + dx, py + dy, pcolor)
- end
- end
- end
- end
- else
- circb(px - psize//2, py - psize//2, psize//2, pcolor)
- circb(px + psize//2, py - psize//2, psize//2, pcolor)
- line(px - psize, py, px, py + psize, pcolor)
- line(px + psize, py, px, py + psize, pcolor)
- end
- end
- if outline_color then
- draw_shape(x-1, y, s, true, outline_color)
- draw_shape(x+1, y, s, true, outline_color)
- draw_shape(x, y-1, s, true, outline_color)
- draw_shape(x, y+1, s, true, outline_color)
- end
- draw_shape(x, y, s, filled, color)
- end
- function _init()
- for r=1,GRID_SIZE do
- grid[r]={}
- for c=1,GRID_SIZE do
- grid[r][c]=0
- end
- end
- generate_shapes()
- clearing_cells,particles,score_popups,line_clear_effects={},{},{},{}
- bg_time=0
- music_time=0
- title_glow=0
- title_float=0
- score=0
- best_score=best_score or 0
- combo=0
- grace_remaining=0
- warning_active=false
- flash_color=12
- heart_beat_timer=0
- turns_since_line_clear=0
- line_guarantee_active=false
- setupMusic()
- if music_playing then
- music(0, -1, -1, true) -- Start background music
- end
- end
- function can_place_shape(shape,row,col)
- for r=1,#shape do
- for c=1,#shape[r] do
- if shape[r][c]==1 then
- local nr=row+r-1
- local nc=col+c-1
- if nr<1 or nr>GRID_SIZE or nc<1 or nc>GRID_SIZE or grid[nr][nc]~=0 then
- return false
- end
- end
- end
- end
- return true
- end
- function place_shape(shape,color,row,col)
- for r=1,#shape do
- for c=1,#shape[r] do
- if shape[r][c]==1 then
- grid[row+r-1][col+c-1]=color
- spawn_place_particles(row+r-1,col+c-1,color)
- end
- end
- end
- end
- function check_clears()
- local cells={}
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do
- if grid[r][c]==0 then full=false break end
- end
- if full then
- for c=1,GRID_SIZE do table.insert(cells,{r,c,grid[r][c]}) end
- table.insert(line_clear_effects,{type="row",pos=r,life=18,max_life=18})
- end
- end
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do
- if grid[r][c]==0 then full=false break end
- end
- if full then
- for r=1,GRID_SIZE do
- local dup=false
- for _,cell in ipairs(cells) do
- if cell[1]==r and cell[2]==c then dup=true break end
- end
- if not dup then table.insert(cells,{r,c,grid[r][c]}) end
- end
- table.insert(line_clear_effects,{type="col",pos=c,life=18,max_life=18})
- end
- end
- return cells
- end
- function clear_cells(c)
- if #c==0 then return end
- clearing_cells=c
- flash=FLASH_FRAMES
- flash_color=streak_ui_color(combo+1)
- local cells_cleared=#c
- local line_multiplier=1+math.min(cells_cleared/8*0.5,1.5)
- local combo_bonus=1+math.min(combo*0.1,0.8)
- shake=math.min(cells_cleared*SHAKE_MULT*line_multiplier*combo_bonus,SHAKE_MAX*2)
- wave_effect=WAVE_TIME
- for _,cell in ipairs(c) do
- spawn_explosion_particles(cell[1],cell[2],cell[3])
- end
- combo=combo+1
- grace_remaining=grace_max
- exit_warning()
- spawn_streak_particles(combo)
- sfx_clear(#c)
- sfx_streak_up(combo)
- add_score_popup("x"..string.format("%.1f",streak_multiplier()),120,90)
- turns_since_line_clear = 0
- line_guarantee_active = false
- end
- function spawn_explosion_particles(row,col,color)
- local x=GRID_X+(col-1)*(CELL_SIZE+1)+CELL_SIZE/2
- local y=GRID_Y+(row-1)*(CELL_SIZE+1)+CELL_SIZE/2
- for i=1,24 do
- local a=math.pi*2*i/24
- local speed = 1.5 + math.random() * 2.5
- table.insert(particles,{
- x=x,y=y,
- vx=math.cos(a)*speed,
- vy=math.sin(a)*speed,
- life=300,color=color,size=2,trail=true
- })
- end
- for i=1,12 do
- table.insert(particles,{
- x=x+math.random(-4,4),y=y+math.random(-4,4),
- vx=(math.random()-0.5)*0.5,vy=math.random()*-1.5,
- life=200,color=12,size=1,sparkle=true
- })
- end
- for i=1,16 do
- local a=math.pi*2*i/16 + math.random()*0.5
- local speed = 0.8 + math.random() * 1.2
- table.insert(particles,{
- x=x+math.random(-2,2),y=y+math.random(-2,2),
- vx=math.cos(a)*speed,
- vy=math.sin(a)*speed,
- life=250,color=14,size=1
- })
- end
- end
- function spawn_place_particles(row,col,color)
- local x=GRID_X+(col-1)*(CELL_SIZE+1)+CELL_SIZE/2
- local y=GRID_Y+(row-1)*(CELL_SIZE+1)+CELL_SIZE/2
- for i=1,5 do
- table.insert(particles,{
- x=x+math.random(-2,2),y=y+math.random(-2,2),
- vx=(math.random()-0.5)*0.8,vy=math.random()*-1.5,
- life=18,color=color,size=1
- })
- end
- end
- function add_score_popup(points_or_text,x,y)
- local text=type(points_or_text)=="number" and ("+"..points_or_text) or tostring(points_or_text)
- table.insert(score_popups,{text=text,x=x,y=y,life=60,scale=2})
- end
- function spawn_streak_particles(level)
- local cx = GRID_X + (GRID_SIZE*(CELL_SIZE+1))//2
- local cy = GRID_Y - 10
- local count = 6 + math.min(level,10)*2
- if level>=5 then count=count+6 end
- for i=1,count do
- local a = (i/count) * 6.28318
- local spd = 1.4 + 0.12*level + (math.random()*0.5)
- table.insert(particles,{
- x=cx, y=cy,
- vx=math.cos(a)*spd,
- vy=math.sin(a)*spd*0.6 - 0.3,
- life=28 + math.min(level,10)*3,
- color= (level>=5) and get_rainbow_color((i*36+rainbow_offset)%360) or (11 - (i%3)),
- size=2 + math.floor(level/3),
- streak_particle=true
- })
- end
- end
- function update_particles()
- for i=#particles,1,-1 do
- local p=particles[i]
- if not (p and p.x and p.y and p.life) then
- table.remove(particles,i)
- else
- p.x=p.x+p.vx; p.y=p.y+p.vy
- p.vy=p.vy+0.08; p.vx=p.vx*0.985
- p.life=p.life-1
- if p.sparkle and p.life%4==0 then
- p.color = (p.color % 15) + 1
- end
- if p.trail and p.life%3==0 then
- table.insert(particles,{x=p.x,y=p.y,vx=0,vy=0,life=10,color=14,size=1})
- end
- if p.x<-20 or p.x>260 or p.y<-20 or p.y>160 or p.life<=0 then table.remove(particles,i) end
- end
- end
- end
- function update_score_popups()
- for i=#score_popups,1,-1 do
- local p=score_popups[i]
- p.y=p.y-0.8; p.life=p.life-1; p.scale=p.scale+(2.5-p.scale)*0.12
- if p.life<=0 then table.remove(score_popups,i) end
- end
- end
- function update_line_effects()
- for i=#line_clear_effects,1,-1 do
- local e=line_clear_effects[i]; e.life=e.life-1
- if e.life<=0 then table.remove(line_clear_effects,i) end
- end
- end
- function get_grid_pos(mx,my)
- local rx=mx-GRID_X; local ry=my-GRID_Y
- if rx<0 or ry<0 then return nil end
- local col=math.floor(rx/(CELL_SIZE+1))+1
- local row=math.floor(ry/(CELL_SIZE+1))+1
- if row>=1 and row<=GRID_SIZE and col>=1 and col<=GRID_SIZE then
- return row,col
- end
- end
- function check_game_over()
- if #clearing_cells>0 or flash>0 then return false end
- if not next_shapes or #next_shapes == 0 then return true end
- for _,sd in ipairs(next_shapes) do
- if sd and sd.shape then
- local s=sd.shape
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if can_place_shape(s,r,c) then
- return false -- Found a valid placement, not game over
- end
- end
- end
- end
- end
- return true -- No valid placements found, game over
- end
- local function fair_clone(src)
- local g={}
- for r=1,GRID_SIZE do g[r]={}; for c=1,GRID_SIZE do g[r][c]=src[r][c] end end
- return g
- end
- local function fair_can(g,s,r,c)
- for i=1,#s do for j=1,#s[i] do
- if s[i][j]==1 then
- local rr=r+i-1; local cc=c+j-1
- if rr<1 or rr>GRID_SIZE or cc<1 or cc>GRID_SIZE or g[rr][cc]~=0 then return false end
- end
- end end
- return true
- end
- local function fair_place(g,s,r,c)
- for i=1,#s do for j=1,#s[i] do if s[i][j]==1 then g[r+i-1][c+j-1]=1 end end end
- end
- local function fair_clear_once(g)
- local cleared=false
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do if g[r][c]==0 then full=false break end end
- if full then for c=1,GRID_SIZE do g[r][c]=0 end cleared=true end
- end
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do if g[r][c]==0 then full=false break end end
- if full then for r=1,GRID_SIZE do g[r][c]=0 end cleared=true end
- end
- return cleared
- end
- local function fair_clear_all(g) while fair_clear_once(g) do end end
- local function fair_list(g,s)
- local t={}
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if fair_can(g,s,r,c) then t[#t+1]={r=r,c=c} end
- end
- end
- return t
- end
- local function get_difficulty_level()
- if score < 500 then return 1 end
- if score < 1500 then return 2 end
- if score < 4000 then return 3 end
- if score < 8000 then return 4 end
- if score < 12000 then return 5 end
- if score < 20000 then return 6 end
- if score < 35000 then return 7 end
- if score < 60000 then return 8 end
- if score < 100000 then return 9 end
- return 10
- end
- local function get_difficulty_params(level)
- if level == 1 then return {min_solutions = 8, max_complexity = 3.5, line_guarantee = false} end
- if level == 2 then return {min_solutions = 6, max_complexity = 5.0, line_guarantee = false} end
- if level == 3 then return {min_solutions = 4, max_complexity = 6.5, line_guarantee = false} end
- if level == 4 then return {min_solutions = 2, max_complexity = 8.0, line_guarantee = false} end
- if level == 5 then return {min_solutions = 2, max_complexity = 9.5, line_guarantee = false} end
- if level == 6 then return {min_solutions = 1, max_complexity = 10.5, line_guarantee = false} end
- if level == 7 then return {min_solutions = 1, max_complexity = 12.0, line_guarantee = false} end
- if level == 8 then return {min_solutions = 1, max_complexity = 13.5, line_guarantee = false} end
- if level == 9 then return {min_solutions = 2, max_complexity = 14.0, line_guarantee = true} end
- if level == 10 then return {min_solutions = 3, max_complexity = 15.0, line_guarantee = true} end
- return {min_solutions = 3, max_complexity = 15.0, line_guarantee = true}
- end
- local function get_shape_complexity(shape)
- local cells = 0
- local max_width = 0
- local max_height = #shape
- for r = 1, #shape do
- local row_width = 0
- for c = 1, #shape[r] do
- if shape[r][c] == 1 then
- cells = cells + 1
- row_width = row_width + 1
- end
- end
- max_width = math.max(max_width, row_width)
- end
- local awkwardness = 0
- if max_width == 1 and max_height > 3 then awkwardness = 2 end
- if max_height == 1 and max_width > 4 then awkwardness = 2 end
- if cells >= 6 then awkwardness = awkwardness + 1 end
- local has_corners = false
- for r = 1, #shape do
- for c = 1, #shape[r] do
- if shape[r][c] == 1 then
- local neighbors = 0
- if r > 1 and shape[r-1][c] == 1 then neighbors = neighbors + 1 end
- if r < #shape and shape[r+1] and shape[r+1][c] == 1 then neighbors = neighbors + 1 end
- if c > 1 and shape[r][c-1] == 1 then neighbors = neighbors + 1 end
- if c < #shape[r] and shape[r][c+1] == 1 then neighbors = neighbors + 1 end
- if neighbors == 2 then has_corners = true end
- end
- end
- end
- if has_corners then awkwardness = awkwardness + 1 end
- return cells + awkwardness
- end
- local function wb_is_awkward(shape)
- local corners = false
- for r=1,#shape do
- for c=1,#shape[r] do
- if shape[r][c]==1 then
- local n=0
- if r>1 and shape[r-1][c]==1 then n=n+1 end
- if r<#shape and shape[r+1] and shape[r+1][c]==1 then n=n+1 end
- if c>1 and shape[r][c-1]==1 then n=n+1 end
- if c<#shape[r] and shape[r][c+1]==1 then n=n+1 end
- if n==2 then corners=true end
- end
- end
- end
- return corners
- end
- local function wb_cell_count(shape)
- local n=0
- for r=1,#shape do
- for c=1,#shape[r] do
- if shape[r][c]==1 then n=n+1 end
- end
- end
- return n
- end
- local function wb_classify_shape(shape)
- local cells = wb_cell_count(shape)
- if wb_is_awkward(shape) and cells>=5 then return "awk" end
- if cells<=2 then return "micro"
- elseif cells<=3 then return "small"
- elseif cells<=5 then return "medium"
- elseif cells>=7 then return "large"
- else return "medium" end
- end
- local function wb_any_fit(shape)
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if can_place_shape(shape, r, c) then return true end
- end
- end
- return false
- end
- local function wb_board_metrics()
- local seen = {}
- for r=1,GRID_SIZE do seen[r]={} end
- local function bfs(sr,sc)
- local q={{sr,sc}}; seen[sr][sc]=true
- local size=0
- local dirs={{1,0},{-1,0},{0,1},{0,-1}}
- while #q>0 do
- local cur=table.remove(q)
- local r,c = cur[1],cur[2]
- size = size + 1
- for _,d in ipairs(dirs) do
- local nr, nc = r+d[1], c+d[2]
- if nr>=1 and nr<=GRID_SIZE and nc>=1 and nc<=GRID_SIZE then
- if not seen[nr][nc] and grid[nr][nc]==0 then
- seen[nr][nc]=true
- q[#q+1]={nr,nc}
- end
- end
- end
- end
- return size
- end
- local free_cells=0
- for r=1,GRID_SIZE do for c=1,GRID_SIZE do if grid[r][c]==0 then free_cells=free_cells+1 end end end
- local largest=0
- local holes=0
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if grid[r][c]==0 and not seen[r][c] then
- local comp=bfs(r,c)
- if comp==1 then holes=holes+1 end
- if comp>largest then largest=comp end
- end
- end
- end
- return {free=free_cells, largest=largest, holes=holes}
- end
- local function wb_base_weights()
- local w = {}
- for i,sh in ipairs(shapes) do
- local cls = wb_classify_shape(sh)
- local v = (cls=="micro" and WB_W_MICRO)
- or (cls=="small" and WB_W_SMALL)
- or (cls=="medium" and WB_W_MEDIUM)
- or (cls=="large" and WB_W_LARGE)
- or WB_W_AWK
- w[i]={w=v, class=cls}
- end
- return w
- end
- local function wb_apply_board_tweaks(weights)
- local m = wb_board_metrics()
- local cramped = (m.largest < WB_CRAMPED_LARGEST_PATCH) or (m.holes >= WB_CRAMPED_HOLES)
- local function tweak(cls, delta)
- for i,rec in ipairs(weights) do
- if rec.class==cls then rec.w = math.max(0.001, rec.w * (1+delta)) end
- end
- end
- if cramped then
- tweak("small", 0.25)
- tweak("micro", 0.15)
- tweak("large", -0.10)
- tweak("awk", -0.10)
- else
- if m.free>=40 then
- tweak("medium", 0.10)
- tweak("large", 0.10)
- end
- end
- end
- local function wb_apply_quotas(weights)
- local count={micro=0, small=0, medium=0, large=0, awk=0}
- for _,cls in ipairs(wb_recent_classes) do count[cls]=(count[cls] or 0)+1 end
- local function cap(cls, maxn)
- if count[cls] and count[cls] >= maxn then
- for _,rec in ipairs(weights) do
- if rec.class==cls then rec.w = 0.001 end
- end
- end
- end
- cap("micro", WB_MAX_MICRO_IN_WIN)
- cap("small", WB_MAX_SMALL_IN_WIN)
- cap("medium",WB_MAX_MEDIUM_IN_WIN)
- cap("large", WB_MAX_LARGE_IN_WIN)
- cap("awk", WB_MAX_AWK_IN_WIN)
- end
- local function wb_sample_k(weights, k)
- local pool = {}
- for i,rec in ipairs(weights) do if rec.w>0 then pool[#pool+1]={i=i,w=rec.w,class=rec.class} end end
- local chosen={}
- for _=1,k do
- local total=0
- for _,p in ipairs(pool) do total=total+p.w end
- if total<=0 or #pool==0 then break end
- local r = math.random()*total
- local acc=0
- local pick_idx=1
- for j,p in ipairs(pool) do
- acc = acc + p.w
- if r<=acc then pick_idx=j; break end
- end
- local pick = pool[pick_idx]
- table.insert(chosen, {idx=pick.i, class=pick.class})
- table.remove(pool, pick_idx)
- end
- return chosen
- end
- local function wb_push_history(classes)
- for _,cls in ipairs(classes) do
- table.insert(wb_recent_classes, cls)
- end
- while #wb_recent_classes > WB_WINDOW do
- table.remove(wb_recent_classes, 1)
- end
- end
- local function wb_triplet_has_fit(triplet)
- for _,pick in ipairs(triplet) do
- if wb_any_fit(shapes[pick.idx]) then return true end
- end
- return false
- end
- local function wb_lerp(a,b,t) return a+(b-a)*t end
- local function wb_health()
- local m = wb_board_metrics() -- you already added this in my patch
- local free_norm = math.min(1, m.free / 40)
- local patch_norm = math.min(1, m.largest / 12)
- local hole_penalty= math.min(1, m.holes / 5)
- local raw = (free_norm*0.6 + patch_norm*0.6) - hole_penalty*0.5
- return math.max(0, math.min(1, raw))
- end
- local function wb_plug_score(shape)
- local cells = wb_cell_count(shape)
- if cells==1 then return 1.0 end
- if cells==2 then return 0.7 end
- if cells<=3 then return 0.4 end
- return 0
- end
- local function count_solution_paths(shape_indices)
- local orderings = {
- {1,2,3}, {1,3,2}, {2,1,3}, {2,3,1}, {3,1,2}, {3,2,1}
- }
- local solution_count = 0
- local max_solutions_to_check = 20
- for _, order in ipairs(orderings) do
- if solution_count >= max_solutions_to_check then break end
- local shape1_idx = shape_indices[order[1]]
- local shape2_idx = shape_indices[order[2]]
- local shape3_idx = shape_indices[order[3]]
- local spots1 = fair_list(grid, shapes[shape1_idx])
- for _, pos1 in ipairs(spots1) do
- if solution_count >= max_solutions_to_check then break end
- local test_grid1 = fair_clone(grid)
- fair_place(test_grid1, shapes[shape1_idx], pos1.r, pos1.c)
- local spots2 = fair_list(test_grid1, shapes[shape2_idx])
- for _, pos2 in ipairs(spots2) do
- if solution_count >= max_solutions_to_check then break end
- local test_grid2 = fair_clone(test_grid1)
- fair_place(test_grid2, shapes[shape2_idx], pos2.r, pos2.c)
- local spots3 = fair_list(test_grid2, shapes[shape3_idx])
- if #spots3 > 0 then solution_count = solution_count + 1 end
- end
- end
- end
- return solution_count
- end
- local function check_for_line_completion_in_trio(trio_indices)
- local orderings = {
- {1,2,3}, {1,3,2}, {2,1,3}, {2,3,1}, {3,1,2}, {3,2,1}
- }
- for _, order in ipairs(orderings) do
- local shape1_idx = trio_indices[order[1]]
- local shape2_idx = trio_indices[order[2]]
- local shape3_idx = trio_indices[order[3]]
- local spots1 = fair_list(grid, shapes[shape1_idx])
- for _, pos1 in ipairs(spots1) do
- local test_grid1 = fair_clone(grid)
- fair_place(test_grid1, shapes[shape1_idx], pos1.r, pos1.c)
- fair_clear_all(test_grid1)
- local line_cleared = false
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do if grid[r][c]==0 then full=false break end end
- if full then
- for c=1,GRID_SIZE do if test_grid1[r][c]==0 then line_cleared=true break end end
- end
- if line_cleared then break end
- end
- if not line_cleared then
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do if grid[r][c]==0 then full=false break end end
- if full then
- for r=1,GRID_SIZE do if test_grid1[r][c]==0 then line_cleared=true break end end
- end
- if line_cleared then break end
- end
- end
- if line_cleared then return true end
- local spots2 = fair_list(test_grid1, shapes[shape2_idx])
- for _, pos2 in ipairs(spots2) do
- local test_grid2 = fair_clone(test_grid1)
- fair_place(test_grid2, shapes[shape2_idx], pos2.r, pos2.c)
- fair_clear_all(test_grid2)
- local line_cleared2 = false
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do if test_grid1[r][c]==0 then full=false break end end
- if full then
- for c=1,GRID_SIZE do if test_grid2[r][c]==0 then line_cleared2=true break end end
- end
- if line_cleared2 then break end
- end
- if not line_cleared2 then
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do if test_grid1[r][c]==0 then full=false break end end
- if full then
- for r=1,GRID_SIZE do if test_grid2[r][c]==0 then line_cleared2=true break end end
- end
- if line_cleared2 then break end
- end
- end
- if line_cleared2 then return true end
- local spots3 = fair_list(test_grid2, shapes[shape3_idx])
- if #spots3 > 0 then
- local test_grid3 = fair_clone(test_grid2)
- fair_place(test_grid3, shapes[shape3_idx], spots3[1].r, spots3[1].c)
- fair_clear_all(test_grid3)
- local line_cleared3 = false
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do if test_grid2[r][c]==0 then full=false break end end
- if full then
- for c=1,GRID_SIZE do if test_grid3[r][c]==0 then line_cleared3=true break end end
- end
- if line_cleared3 then break end
- end
- if not line_cleared3 then
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do if test_grid2[r][c]==0 then full=false break end end
- if full then
- for r=1,GRID_SIZE do if test_grid3[r][c]==0 then line_cleared3=true break end end
- end
- if line_cleared3 then break end
- end
- end
- if line_cleared3 then return true end
- end
- end
- end
- end
- return false
- end
- function wb_generate_shapes()
- local pity = (wb_consec_dead >= WB_PITY_CAP)
- local H = wb_health()
- local R_eff = math.max(1, math.floor(wb_lerp(WB_R_TRIES+1, WB_R_TRIES-1, H)))
- local p_fail_eff = wb_lerp(0.05, 0.25, H) -- cramped => ~5%, open => ~25%
- local weights = wb_base_weights()
- wb_apply_board_tweaks(weights)
- wb_apply_quotas(weights)
- local m = wb_board_metrics()
- if m.holes >= WB_CRAMPED_HOLES then
- for i,rec in ipairs(weights) do
- local s = wb_plug_score(shapes[i])
- if s>0 then rec.w = rec.w * (1 + 0.3*s) end -- up to +30%
- end
- end
- if combo >= 3 then
- for _,rec in ipairs(weights) do
- if rec.class=="micro" then rec.w = rec.w * 0.90 end
- if rec.class=="large" or rec.class=="awk" then rec.w = rec.w * 1.10 end
- end
- elseif combo==0 and turns_since_line_clear>0 then
- for _,rec in ipairs(weights) do
- if rec.class=="small" then rec.w = rec.w * 1.10 end
- end
- end
- local last_triplet = nil
- local had_fit = false
- for t=1,R_eff do
- local sample = wb_sample_k(weights, 3)
- if #sample==3 then
- last_triplet = sample
- if wb_triplet_has_fit(sample) then
- had_fit = true
- break
- end
- end
- end
- if not last_triplet or #last_triplet<3 then
- local picked={}
- while #picked<3 do
- local i=math.random(#shapes)
- local ok=true
- for _,p in ipairs(picked) do if p.idx==i then ok=false break end end
- if ok then picked[#picked+1]={idx=i, class=wb_classify_shape(shapes[i])} end
- end
- last_triplet = picked
- had_fit = wb_triplet_has_fit(last_triplet)
- end
- if not last_triplet or #last_triplet < 3 then
- last_triplet = {
- {idx=1, class="micro"}, -- single dot
- {idx=2, class="micro"}, -- double dot
- {idx=3, class="micro"} -- vertical double
- }
- end
- if not had_fit and not pity then
- if math.random() < p_fail_eff then
- wb_consec_dead = wb_consec_dead + 1
- else
- for _=1,5 do
- local sample = wb_sample_k(weights, 3)
- if #sample==3 then
- last_triplet = sample
- if wb_triplet_has_fit(sample) then
- had_fit = true
- break
- end
- end
- end
- wb_consec_dead = 0
- end
- else
- wb_consec_dead = 0
- end
- if turns_since_line_clear >= 3 then
- local has_saver=false
- for _,p in ipairs(last_triplet) do
- if p.class=="micro" or p.class=="small" then has_saver=true break end
- end
- if not has_saver then
- local worst_i=1
- local priority = {awk=5, large=4, medium=3, small=2, micro=1}
- for i=2,3 do
- if priority[last_triplet[i].class] > priority[last_triplet[worst_i].class] then
- worst_i=i
- end
- end
- local candidates={}
- for i,rec in ipairs(wb_base_weights()) do
- if rec.class=="micro" or rec.class=="small" then
- table.insert(candidates, {idx=i,class=rec.class,w=rec.w})
- end
- end
- if #candidates>0 then
- local best = candidates[ math.random(#candidates) ]
- last_triplet[worst_i] = {idx=best.idx, class=best.class}
- had_fit = had_fit or wb_any_fit(shapes[best.idx])
- end
- end
- end
- local m = wb_board_metrics()
- local open = (m.free >= 40 and m.largest >= 10)
- local function count_big(tri)
- local n=0; for _,p in ipairs(tri) do if p.class=="large" or p.class=="awk" then n=n+1 end end
- return n
- end
- if not open and count_big(last_triplet) > 1 then
- local sample2 = wb_sample_k(weights,3)
- if #sample2==3 and count_big(sample2)<=1 then last_triplet=sample2 end
- end
- if wb_consec_dead >= 2 then
- local best_i, best_count = 1, -1
- for i,p in ipairs(last_triplet) do
- local count = 0
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if can_place_shape(shapes[p.idx], r, c) then count = count + 1 end
- end
- end
- if count > best_count then best_count = count; best_i = i end
- end
- if best_count < 4 then
- local candidates={}
- for i,rec in ipairs(wb_base_weights()) do
- if rec.class=="micro" or rec.class=="small" then
- local cnt = 0
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- if can_place_shape(shapes[i], r, c) then cnt = cnt + 1 end
- end
- end
- if cnt>=4 then table.insert(candidates,{idx=i,class=rec.class}) end
- end
- end
- if #candidates>0 then
- local pick = candidates[math.random(#candidates)]
- last_triplet[1] = {idx=pick.idx, class=pick.class}
- had_fit = had_fit or wb_any_fit(shapes[pick.idx])
- end
- end
- end
- local order = {1,2,3}
- for i=#order,2,-1 do local j=math.random(i) order[i],order[j]=order[j],order[i] end
- next_shapes = {}
- for k=1,3 do
- local pick = last_triplet[order[k]]
- if not pick or not pick.idx or not shapes[pick.idx] then
- pick = {idx = 1, class = "micro"}
- end
- next_shapes[k] = {
- shape = shapes[pick.idx],
- color = colors[math.random(#colors)],
- id = k,
- spawn_time = time()
- }
- end
- local classes = {}
- for i=1,3 do
- if last_triplet[i] and last_triplet[i].class then
- table.insert(classes, last_triplet[i].class)
- else
- table.insert(classes, "micro") -- fallback class
- end
- end
- wb_push_history(classes)
- end
- function enhanced_generate_shapes()
- local difficulty = get_difficulty_level()
- local params = get_difficulty_params(difficulty)
- local min_solutions = params.min_solutions
- local max_avg_complexity = params.max_complexity
- local line_guarantee = params.line_guarantee
- local max_attempts = 300
- for attempt = 1, max_attempts do
- local trio_indices = {}
- local used_shapes = {}
- local total_complexity = 0
- for i = 1, 3 do
- local shape_idx
- local attempts_for_shape = 0
- repeat
- local base_roll = math.random()
- if base_roll < 0.15 then
- repeat
- shape_idx = math.random(#shapes)
- until get_shape_complexity(shapes[shape_idx]) >= 3
- elseif base_roll < 0.35 then
- repeat
- shape_idx = math.random(#shapes)
- until get_shape_complexity(shapes[shape_idx]) >= 4
- else
- shape_idx = math.random(#shapes)
- end
- attempts_for_shape = attempts_for_shape + 1
- until (not used_shapes[shape_idx] and attempts_for_shape < 50)
- if attempts_for_shape >= 50 then goto continue end
- used_shapes[shape_idx] = true
- table.insert(trio_indices, shape_idx)
- total_complexity = total_complexity + get_shape_complexity(shapes[shape_idx])
- end
- if #trio_indices ~= 3 then goto continue end
- local avg_complexity = total_complexity / 3
- if avg_complexity > max_avg_complexity then goto continue end
- if difficulty <= 2 and total_complexity > 12 then goto continue end
- local solution_count = count_solution_paths(trio_indices)
- local line_requirement_met = true
- if line_guarantee and turns_since_line_clear >= 2 then
- line_guarantee_active = true
- line_requirement_met = check_for_line_completion_in_trio(trio_indices)
- if line_requirement_met then
- local complexity_variance = 0
- for i = 1, 3 do
- local shape_complexity = get_shape_complexity(shapes[trio_indices[i]])
- complexity_variance = complexity_variance + math.abs(shape_complexity - (total_complexity / 3))
- end
- if complexity_variance < 1.5 then
- line_requirement_met = math.random() < 0.7 -- 70% chance to accept low variance
- end
- if line_requirement_met then
- local total_placement_options = 0
- for i = 1, 3 do
- local placement_count = #fair_list(grid, shapes[trio_indices[i]])
- total_placement_options = total_placement_options + placement_count
- end
- if total_placement_options < 8 then
- line_requirement_met = math.random() < 0.6 -- Reduce acceptance for obvious placements
- end
- end
- end
- elseif line_guarantee and turns_since_line_clear >= 1 and math.random() < 0.15 then
- line_requirement_met = check_for_line_completion_in_trio(trio_indices)
- end
- if solution_count >= min_solutions and line_requirement_met then
- next_shapes = {}
- local order = {1, 2, 3}
- for i = #order, 2, -1 do
- local j = math.random(i)
- order[i], order[j] = order[j], order[i]
- end
- for k = 1, 3 do
- local idx = trio_indices[order[k]]
- next_shapes[k] = {
- shape = shapes[idx],
- color = colors[math.random(#colors)],
- id = k,
- spawn_time = time()
- }
- end
- return
- end
- ::continue::
- end
- emergency_generate_shapes()
- end
- function emergency_generate_shapes()
- local simple_shapes = {}
- for i, shape in ipairs(shapes) do
- local complexity = get_shape_complexity(shape)
- table.insert(simple_shapes, {idx = i, complexity = complexity})
- end
- table.sort(simple_shapes, function(a, b) return a.complexity < b.complexity end)
- for i = 1, math.min(10, #simple_shapes) do
- for j = i + 1, math.min(15, #simple_shapes) do
- for k = j + 1, math.min(20, #simple_shapes) do
- local trio = {simple_shapes[i].idx, simple_shapes[j].idx, simple_shapes[k].idx}
- if count_solution_paths(trio) >= 1 then
- next_shapes = {}
- for l = 1, 3 do
- next_shapes[l] = {
- shape = shapes[trio[l]],
- color = colors[math.random(#colors)],
- id = l,
- spawn_time = time()
- }
- end
- return
- end
- end
- end
- end
- next_shapes = {}
- for i = 1, 3 do
- next_shapes[i] = { shape = {{1}}, color = colors[math.random(#colors)], id = i, spawn_time = time() }
- end
- end
- function generate_shapes()
- if USE_WEIGHTED_BAG then
- wb_generate_shapes()
- else
- enhanced_generate_shapes() -- your original "always solvable" generator
- end
- end
- local function clone_grid()
- local g={}
- for r=1,GRID_SIZE do
- g[r]={}
- for c=1,GRID_SIZE do g[r][c]=grid[r][c] end
- end
- return g
- end
- local function calc_clears_for_grid(g)
- local cells={}
- local seen={}
- for r=1,GRID_SIZE do seen[r]={} end
- for r=1,GRID_SIZE do
- local full=true
- for c=1,GRID_SIZE do
- if g[r][c]==0 then full=false break end
- end
- if full then
- for c=1,GRID_SIZE do
- if not seen[r][c] then
- table.insert(cells,{r,c,g[r][c]}); seen[r][c]=true
- end
- end
- end
- end
- for c=1,GRID_SIZE do
- local full=true
- for r=1,GRID_SIZE do
- if g[r][c]==0 then full=false break end
- end
- if full then
- for r=1,GRID_SIZE do
- if not seen[r][c] then
- table.insert(cells,{r,c,g[r][c]}); seen[r][c]=true
- end
- end
- end
- end
- return cells
- end
- local function simulate_clears_if_placed(shape, base_row, base_col)
- local g=clone_grid()
- for r=1,#shape do
- for c=1,#shape[r] do
- if shape[r][c]==1 then
- local rr=base_row + r - 1
- local cc=base_col + c - 1
- if rr<1 or rr>GRID_SIZE or cc<1 or cc>GRID_SIZE then
- return {}
- end
- if g[rr][cc]~=0 then
- return {}
- end
- g[rr][cc]=1
- end
- end
- end
- return calc_clears_for_grid(g)
- end
- function preview_cell_size(shape)
- local h=#shape; local w=#shape[1]
- local fit=math.floor((PREVIEW_BOX-4)/math.max(w,h))
- if fit<1 then fit=1 end
- local bs=math.min(PREVIEW_CELL_CAP, fit)
- if bs<PREVIEW_MIN_CELL then bs=PREVIEW_MIN_CELL end
- return bs
- end
- function TIC()
- local mx,my,left=mouse(); mouse_x=mx; mouse_y=my
- rainbow_offset=(rainbow_offset+2)%360
- title_glow = (title_glow + 0.15) % (math.pi * 2)
- title_float = (title_float + 0.08) % (math.pi * 2)
- heart_beat_timer = heart_beat_timer + 1
- if wave_effect>0 then wave_effect=wave_effect-1 end
- if left and not dragging then
- for i,_ in ipairs(next_shapes) do
- local cx=RIGHT_PANEL_X
- local cy=RIGHT_PANEL_TOP+(i-1)*(PREVIEW_BOX+PREVIEW_GAP)
- if mx>=cx and mx<=cx+PREVIEW_BOX and my>=cy and my<=cy+PREVIEW_BOX then
- local sd=next_shapes[i]
- local s=sd.shape
- local h=#s; local w=#s[1]
- local bs=preview_cell_size(s)
- local sx0=cx+PREVIEW_BOX//2 - (w*bs)//2
- local sy0=cy+PREVIEW_BOX//2 - (h*bs)//2
- local grab_r,grab_c=nil,nil
- for r=1,h do
- for c=1,w do
- if s[r][c]==1 then
- local bx=sx0+(c-1)*bs
- local by=sy0+(r-1)*bs
- if mx>=bx and mx<=bx+bs-1 and my>=by and my<=by+bs-1 then
- grab_r=r; grab_c=c
- end
- end
- end
- end
- if not grab_r then
- for r=1,h do
- for c=1,w do
- if s[r][c]==1 then grab_r=r; grab_c=c; break end
- end
- if grab_r then break end
- end
- end
- dragging={shape=sd.shape,color=sd.color,index=i,grab_r=grab_r,grab_c=grab_c}
- sfx_pick()
- break
- end
- end
- elseif not left and dragging then
- local row,col=get_grid_pos(mx,my)
- if row and col then
- local base_row = row - (dragging.grab_r-1)
- local base_col = col - (dragging.grab_c-1)
- if can_place_shape(dragging.shape, base_row, base_col) then
- place_shape(dragging.shape, dragging.color, base_row, base_col)
- table.remove(next_shapes,dragging.index)
- turns_since_line_clear = turns_since_line_clear + 1
- local cells=check_clears()
- if #cells>0 then
- clear_cells(cells)
- local raw=10*#cells
- local pts=math.floor(raw*streak_multiplier()+0.5)
- add_score_popup(pts,120,100)
- else
- if combo>0 then
- grace_remaining=math.max(0,grace_remaining-1)
- if grace_remaining==1 then
- flash=FLASH_FRAMES
- flash_color=6
- enter_warning()
- elseif grace_remaining==0 then
- streak_break()
- end
- end
- end
- if #next_shapes==0 then generate_shapes() end
- if #clearing_cells==0 and flash==0 and check_game_over() then
- game_over=true
- music_playing=false
- sfx_game_over()
- end
- sfx_place()
- else
- sfx_invalid()
- end
- else
- sfx_invalid()
- end
- dragging=nil
- end
- if #clearing_cells>0 then
- if flash>0 then flash=flash-1 end
- if shake>0 then shake=shake-0.4 end
- if flash==0 then
- local pts=0
- for _,cell in ipairs(clearing_cells) do
- grid[cell[1]][cell[2]]=0; pts=pts+10
- end
- local final_score = math.floor(pts*streak_multiplier()+0.5)
- score=score+final_score
- if score>best_score then best_score=score end
- clearing_cells={}
- local again=check_clears()
- if #again>0 then
- clear_cells(again)
- else
- if #next_shapes==0 then generate_shapes() end
- if check_game_over() then
- game_over=true
- music_playing=false
- sfx_game_over()
- end
- end
- end
- end
- update_particles(); update_score_popups(); update_line_effects()
- update_music(); update_warning_audio()
- if not game_over and #clearing_cells==0 and flash==0 then
- if check_game_over() then
- game_over=true
- music_playing=false
- sfx_game_over()
- end
- end
- if keyp(7) then -- G key for testing
- game_over = true
- music_playing = false
- sfx_game_over()
- end
- if keyp(18) and game_over then
- _init(); game_over=false; score=0; combo=0; music_playing=true
- grace_remaining=0; warning_active=false; flash_color=12
- turns_since_line_clear=0; line_guarantee_active=false
- sfx_restart()
- end
- if keyp(12) then music_playing = not music_playing end
- draw()
- end
- local function text_w(s, sc) return #s*6*(sc or 1) end
- local function outlined_text(s, x, y, fill, outline, sc)
- sc = sc or 1
- for dx=-1,1 do
- for dy=-1,1 do
- if dx~=0 or dy~=0 then print(s, x+dx, y+dy, outline, false, sc) end
- end
- end
- print(s, x, y, fill, false, sc)
- end
- local function outlined_center(s, y, fill, outline, sc)
- sc = sc or 1
- local x = (240 - text_w(s, sc))//2
- outlined_text(s, x, y, fill, outline, sc)
- end
- local function print_center(s, y, col, sc)
- sc = sc or 1
- local x = (240 - text_w(s, sc))//2
- print(s, x, y, col, false, sc)
- end
- local function get_rainbow_table()
- return {6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
- end
- function get_rainbow_color(offset)
- local colors = get_rainbow_table()
- local idx = math.floor(offset / 36) % #colors + 1
- return colors[idx]
- end
- local function draw_scoreboard(sx, sy)
- local score_x = sx + SCORE_X
- local score_y = sy + SCORE_Y
- rect(score_x-2, score_y-2, SCORE_WIDTH+4, SCORE_HEIGHT+4, 0)
- rect(score_x-1, score_y-1, SCORE_WIDTH+2, SCORE_HEIGHT+2, 14)
- rect(score_x, score_y, SCORE_WIDTH, SCORE_HEIGHT, 2)
- local y1 = score_y + 3
- local y2 = score_y + 14
- local bx = score_x + 4
- local best_s = string.format("%08d", best_score)
- local score_s = string.format("%08d", score)
- print("BEST", bx, y1, 12, false, 1)
- print(best_s, bx + text_w("BEST "), y1, 14, false, 1)
- print("SCORE", bx, y2, 12, false, 1)
- print(score_s, bx + text_w("SCORE "), y2, 11, false, 1)
- if combo > 0 then
- local mult = "x"..string.format("%.1f", streak_multiplier())
- local combo_x = score_x + SCORE_WIDTH + 6
- local combo_w, combo_h = 40, 16
- local combo_y = score_y + (SCORE_HEIGHT - combo_h)//2
- local col = streak_ui_color(combo)
- rect(combo_x-1, combo_y-1, combo_w+2, combo_h+2, 0)
- rect(combo_x, combo_y, combo_w, combo_h, col)
- local cx = combo_x + (combo_w - text_w(mult))//2
- local cy = combo_y + (combo_h - 6)//2
- print(mult, cx, cy, 14, false, 1)
- local heart_y = score_y + SCORE_HEIGHT + 6
- local heart_x = score_x + 8
- for i=1,grace_max do
- local filled = (i <= grace_remaining)
- local hc = filled and 2 or 1
- local shake_x, shake_y = 0, 0
- local beating = false
- if grace_remaining == 1 and i == 1 and filled then
- shake_x = math.sin(time() * 0.15) * 0.3
- shake_y = math.sin(time() * 0.12) * 0.2
- beating = true
- end
- draw_heart(heart_x + (i-1)*12 + shake_x, heart_y + shake_y, 3, filled, hc, 0, beating)
- end
- end
- end
- function draw()
- cls(1)
- rect(0,0,240,136,1)
- local sx,sy=0,0
- if shake>0 then
- sx=math.sin(time()*0.5)*shake; sy=math.cos(time()*0.7)*shake
- end
- draw_animated_background()
- if ENABLE_SCREEN_FLASH and flash>0 and flash%2==0 then
- rect(0,0,240,136,flash_color)
- end
- local title_y = 3 + math.sin(title_float) * 2
- local title = "BLOCK BLAST"
- local x = (240 - text_w(title, 1))//2
- for i=1,#title do
- local char = title:sub(i,i)
- local char_color = get_rainbow_color(rainbow_offset + i*30)
- local char_y = title_y + math.sin(title_float + i*0.3) * 1
- outlined_text(char, x + (i-1)*6, char_y, char_color, 0, 1)
- end
- for i=1,6 do
- local spark_x = x + math.sin(time()*0.003 + i) * 60 + 60
- local spark_y = title_y + math.cos(time()*0.004 + i*2) * 8 + 4
- local spark_color = get_rainbow_color(rainbow_offset + i*60)
- pix(spark_x, spark_y, spark_color)
- end
- local gx=sx+GRID_X; local gy=sy+GRID_Y
- rect(gx-4,gy-4,GRID_SIZE*9+7,GRID_SIZE*9+7,0)
- rect(gx-3,gy-3,GRID_SIZE*9+5,GRID_SIZE*9+5,14)
- rect(gx-2,gy-2,GRID_SIZE*9+3,GRID_SIZE*9+3,2)
- rect(gx-1,gy-1,GRID_SIZE*9+1,GRID_SIZE*9+1,1)
- for r=1,GRID_SIZE do
- for c=1,GRID_SIZE do
- local x=gx+(c-1)*9; local y=gy+(r-1)*9
- local color=grid[r][c]
- local woff=0
- if wave_effect>0 and color==0 then
- woff=math.sin((c+r)*0.5+time()*0.01)*wave_effect*0.03
- end
- if color==0 then
- rect(x,y+woff,CELL_SIZE,CELL_SIZE,1)
- rectb(x,y+woff,CELL_SIZE,CELL_SIZE,2)
- else
- rect(x,y,CELL_SIZE,CELL_SIZE,color)
- line(x,y,x+CELL_SIZE-2,y,14)
- line(x,y,x,y+CELL_SIZE-2,14)
- line(x+CELL_SIZE-1,y+1,x+CELL_SIZE-1,y+CELL_SIZE-1,0)
- line(x+1,y+CELL_SIZE-1,x+CELL_SIZE-1,y+CELL_SIZE-1,0)
- rectb(x,y,CELL_SIZE,CELL_SIZE,0)
- end
- for _,cell in ipairs(clearing_cells) do
- if cell[1]==r and cell[2]==c then
- rectb(x-1,y-1,CELL_SIZE+2,CELL_SIZE+2,14)
- rectb(x-2,y-2,CELL_SIZE+4,CELL_SIZE+4,12)
- end
- end
- end
- end
- for _,e in ipairs(line_clear_effects) do
- local a=e.life/e.max_life
- local w=2+(1-a)*LINE_WIDTH_MAX
- local brightness = math.sin((1-a)*math.pi)
- if e.type=="row" then
- local y=gy+(e.pos-1)*9+4
- rect(gx-w,y-2,GRID_SIZE*9+w*2,5,14)
- rect(gx-w+1,y-1,GRID_SIZE*9+w*2-2,3,12)
- if brightness > 0.5 then rect(gx-w+2,y,GRID_SIZE*9+w*2-4,1,15) end
- else
- local x=gx+(e.pos-1)*9+4
- rect(x-2,gy-w,5,GRID_SIZE*9+w*2,14)
- rect(x-1,gy-w+1,3,GRID_SIZE*9+w*2-2,12)
- if brightness > 0.5 then rect(x,gy-w+2,1,GRID_SIZE*9+w*2-4,15) end
- end
- end
- if dragging then
- local row,col=get_grid_pos(mouse_x,mouse_y)
- if row and col then
- local base_row = row - (dragging.grab_r-1)
- local base_col = col - (dragging.grab_c-1)
- if can_place_shape(dragging.shape, base_row, base_col) then
- local would_clear = simulate_clears_if_placed(dragging.shape, base_row, base_col)
- if #would_clear>0 then
- for _,cell in ipairs(would_clear) do
- local r,c = cell[1], cell[2]
- local x = gx + (c-1)*9
- local y = gy + (r-1)*9
- rect(x,y,CELL_SIZE,CELL_SIZE,dragging.color)
- line(x,y,x+CELL_SIZE-2,y,12)
- line(x,y,x,y+CELL_SIZE-2,12)
- rectb(x,y,CELL_SIZE,CELL_SIZE,0)
- rectb(x-1,y-1,CELL_SIZE+2,CELL_SIZE+2,14)
- rectb(x-2,y-2,CELL_SIZE+4,CELL_SIZE+4,12)
- if time()%20<10 then rectb(x-3,y-3,CELL_SIZE+6,CELL_SIZE+6,15) end
- end
- end
- end
- end
- end
- if dragging then
- local row,col=get_grid_pos(mouse_x,mouse_y)
- if row and col then
- local base_row = row - (dragging.grab_r-1)
- local base_col = col - (dragging.grab_c-1)
- if can_place_shape(dragging.shape, base_row, base_col) then
- local s=dragging.shape
- for r=1,#s do
- for c=1,#s[r] do
- if s[r][c]==1 then
- local x=gx+(base_col + c -2)*9
- local y=gy+(base_row + r -2)*9
- rect(x,y,CELL_SIZE,CELL_SIZE,12)
- rectb(x,y,CELL_SIZE,CELL_SIZE,14)
- if time()%30<15 then rectb(x-1,y-1,CELL_SIZE+2,CELL_SIZE+2,15) end
- end
- end
- end
- else
- local s=dragging.shape
- for r=1,#s do
- for c=1,#s[r] do
- if s[r][c]==1 then
- local x=gx+(base_col + c -2)*9
- local y=gy+(base_row + r -2)*9
- if time()%20<10 then
- rect(x,y,CELL_SIZE,CELL_SIZE,6)
- rectb(x,y,CELL_SIZE,CELL_SIZE,7)
- end
- end
- end
- end
- end
- end
- end
- local streak_level = combo
- for _,p in ipairs(particles) do
- if p and p.life and p.x and p.y then
- if p.life>0 then
- local alpha = p.life / 45
- if p.sparkle then
- if time()%6<3 then
- rect(p.x+sx-1,p.y+sy-1,2,2,p.color)
- else
- pix(p.x+sx,p.y+sy,15)
- end
- elseif p.streak_particle then
- local size = p.size + math.sin(time()*0.1 + p.x*0.1)*0.5
- rect(p.x+sx-size//2,p.y+sy-size//2,size,size,p.color)
- if streak_level >= 5 then circb(p.x+sx,p.y+sy,size+1,15) end
- elseif p.warning then
- local pulse_size = 1 + math.sin(time()*0.2)*0.5
- rect(p.x+sx-pulse_size//2,p.y+sy-pulse_size//2,pulse_size,pulse_size,p.color)
- elseif p.size==2 then
- rect(p.x+sx-1,p.y+sy-1,2,2,p.color)
- if alpha > 0.7 then pix(p.x+sx,p.y+sy,15) end
- else
- pix(p.x+sx,p.y+sy,p.color)
- end
- end
- end
- end
- draw_scoreboard(sx, sy)
- print("NEXT", RIGHT_PANEL_X-2, RIGHT_PANEL_TOP-12, 12, false, 1)
- for i,sd in ipairs(next_shapes) do
- if not dragging or dragging.index~=i then
- local cx=sx+RIGHT_PANEL_X
- local cy=sy+RIGHT_PANEL_TOP+(i-1)*(PREVIEW_BOX+PREVIEW_GAP)
- rect(cx-2, cy-2, PREVIEW_BOX+4, PREVIEW_BOX+4, 0)
- rect(cx-1, cy-1, PREVIEW_BOX+2, PREVIEW_BOX+2, 14)
- rect(cx, cy, PREVIEW_BOX, PREVIEW_BOX, 2)
- local s=sd.shape; local h=#s; local w=#s[1]
- local bs=preview_cell_size(s)
- local sx0=cx+PREVIEW_BOX//2 - (w*bs)//2
- local sy0=cy+PREVIEW_BOX//2 - (h*bs)//2
- for r=1,h do
- for c=1,w do
- if s[r][c]==1 then
- local bx=sx0+(c-1)*bs; local by=sy0+(r-1)*bs
- rect(bx,by,bs-1,bs-1,sd.color)
- line(bx,by,bx+bs-2,by,12)
- line(bx,by,bx,by+bs-2,12)
- line(bx+bs-1,by+1,bx+bs-1,by+bs-1,0)
- line(bx+1,by+bs-1,bx+bs-1,by+bs-1,0)
- rectb(bx,by,bs-1,bs-1,0)
- end
- end
- end
- if mouse_x>=cx and mouse_x<=cx+PREVIEW_BOX and mouse_y>=cy and mouse_y<=cy+PREVIEW_BOX then
- rectb(cx-3,cy-3,PREVIEW_BOX+6,PREVIEW_BOX+6,14)
- if time()%40<20 then rectb(cx-4,cy-4,PREVIEW_BOX+8,PREVIEW_BOX+8,15) end
- end
- end
- end
- if dragging then
- local s=dragging.shape
- local x0 = mouse_x - (dragging.grab_c-1)*8 - 4
- local y0 = mouse_y - (dragging.grab_r-1)*8 - 4
- for r=1,#s do
- for c=1,#s[r] do
- if s[r][c]==1 then
- local x=x0 + (c-1)*8 + 2
- local y=y0 + (r-1)*8 + 2
- rect(x,y,7,7,0)
- end
- end
- end
- for r=1,#s do
- for c=1,#s[r] do
- if s[r][c]==1 then
- local x=x0 + (c-1)*8
- local y=y0 + (r-1)*8
- rect(x,y,7,7,dragging.color)
- line(x,y,x+6,y,12); line(x,y,x,y+6,12)
- line(x+6,y+1,x+6,y+6,0); line(x+1,y+6,x+6,y+6,0)
- rectb(x,y,7,7,0)
- end
- end
- end
- end
- for _,p in ipairs(score_popups) do
- if p and p.life and p.x and p.y and p.text then
- if p.life>0 then
- local s=p.scale or 1
- local alpha = p.life / 60
- local color = alpha > 0.5 and 14 or 12
- if tonumber((p.text or ""):match("%d+")) and tonumber((p.text or ""):match("%d+")) > 100 then
- outlined_text(p.text, (240 - text_w(p.text, s))//2, p.y+sy, color, 6, s)
- else
- outlined_center(p.text, p.y+sy, color, 0, s)
- end
- end
- end
- end
- if music_playing then
- local note_x = 8
- local note_y = 50 + math.sin(time()*0.005)*2
- local beat_pulse = math.sin(music_time*0.02)*0.3 + 0.7
- pix(note_x, note_y, beat_pulse > 0.8 and 12 or 14)
- pix(note_x+1, note_y, beat_pulse > 0.8 and 12 or 14)
- print("♪", note_x+3, note_y-2, beat_pulse > 0.8 and 11 or 12, false, 1)
- end
- if game_over then
- for y=0,136,3 do for x=0,240,3 do pix(x,y,0) pix(x+1,y+1,0) end end
- rect(45,35,150,80,0)
- rect(46,36,148,78,14)
- rect(47,37,146,76,1)
- rect(48,38,144,74,2)
- local go_y = 45 + math.sin(time()*0.005)*1
- outlined_center("GAME OVER", go_y, 6, 0, 2)
- print_center("FINAL SCORE", 70, 14, 1)
- local s = string.format("%08d", score)
- local bw = text_w(s,1) + 16
- local bx = (240 - bw)//2
- rect(bx-1, 81, bw+2, 14, 0)
- rect(bx, 82, bw, 12, 1)
- rect(bx+1, 83, bw-2, 10, 2)
- outlined_text(s, bx+8, 85, 11, 0, 1)
- if (time()%1200)<600 then
- outlined_center("Press R to RESTART", 102, 12, 0, 1)
- end
- print_center("Press M to toggle music", 112, 8, 1)
- end
- end
- _init()
Add Comment
Please, Sign In to add comment