@ -0,0 +1,103 @@
#!/usr/bin/env BQN
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 Rampoina <rampoina@protonmail.com>
#
# Arc
# The level is a 2d matrix of lists (tiles)
# Each list contains the objects of the game:
# 0: floor, 1: player, 2: box, 3: goal, 4: wall
# 4: player on goal, 5: box on goal
#
# Example: ASCII:
# ┌─ ┌─
# ╵ ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ ╵"#####
# ⟨ 4 ⟩ ⟨ 1 0 ⟩ ⟨ 2 0 ⟩ ⟨ 0 ⟩ ⟨ 4 ⟩ #@*.#
# ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ ⟨ 4 ⟩ #####"
# ┘ ┘
moves ←⟨ 0 ‿ 0 ⟩ # list of moves, each move is a direction, we start without moving
chars ←" λ$⊕λ⭍#/\-|+<>^v" # legal characters
movables ←1 ‿ 2 ‿ 7 ‿ 8 ‿ 1 2 ‿ 1 3 ‿ 1 4 ‿ 1 5 # player, box mirrors and lasers
opaque ←1 ‿ 2 ‿ 3 ‿ 1 2 ‿ 1 3 ‿ 1 4 ‿ 1 5 ‿ 6 # non laser reflecting
empties ←0 ‿ 9 ‿ 1 0 ‿ 1 1 # floor and laser beams
ansi ←{
e ⇐@ + 27
red ⇐e ∾ "[31m"
cyan ⇐e ∾ "[36m"
yellow ⇐e ∾ "[33m"
defaultB ⇐e ∾ "[0m"
}
SplitOnEmpty ←{ 𝕩 ⊔ ˜ ( ⊢ - ˜ + ` × ¬ ) 0 = ≠ ¨ 𝕩 }
Ascii2Matrix ←{ ( ⊑ chars ⊐ 𝕩 ) ⊑ ⟨ ≍ 0 , 1 ‿ 0 , 2 ‿ 0 , ≍ 3 , 1 ‿ 3 , 2 ‿ 3 , ≍ 6 , 7 ‿ 0 , 8 ‿ 0 , ≍ 9 , ≍ 10 , ≍ 11 , 12 ‿ 0 , 13 ‿ 0 , 14 ‿ 0 , 15 ‿ 0 ⟩ } ¨ ( ⊢ ↑ ˝ · ≍ ⟜ ¬ 2 + ≢ )
# 𝕨 Tiles 𝕩 | 𝕩 : object coordinate (3‿1) | 𝕨 : direction vector (¯1‿0)
# result: ⟨ ⟨ 3 1 ⟩ ⟨ 2 1 ⟩ ⟨ 1 1 ⟩ ⟩
# returns 3 tiles in the specified direction from the
Tiles ←{ ⟨ 𝕩 , 𝕩 + 𝕨 , 𝕩 + 2 × 𝕨 ⟩ } # given object (including itself)
# Move 𝕩 | 𝕩 : ⟨⟨1,0⟩,⟨0⟩⟩ (2 tiles) | result: ⟨⟨0⟩,⟨1,0⟩⟩
# Move the first object in the first tile to the second tile.
# Only move Movable tile -> Empty tile
# the second tile can't be a movable object because we moved it previously
# if it is it means that the object was unmovable (next to a wall) so we do nothing
Move ←{ a ‿ b : ⟨ 1 ↓ a , ( ⊑ a ) ∾ b ⟩ } ⍟ { ∨ ´ ( ⥊ movables ≍ ⌜ empties ) ≡ ⌜ < ⊑ ¨ 𝕩 }
# Push 𝕩 | 𝕩 : ⟨⟨1,0⟩,⟨2,0⟩,⟨0,0)⟩ (3 tiles) | result: ⟨⟨0⟩,⟨1,0⟩,⟨2,0)⟩
# Given 3 tiles try to [P]ush the second tile (possible box)
# and afterwards try to move the first one (player) if possible
Push ←Move ⌾ ( 2 ↑ ⊢ ) Move ⌾ ( 1 ↓ ⊢ )
FindIdx ←/ ○ ⥊ ⟜ ( ↕ ≢ ) ∘ ⍷
# 𝕨 Beam 𝕩 | 𝕨 : level | 𝕩 : position | replace positions with the laser beam
Beam ←{ { "--++" ⊏ ˜ " -|+" ⊐ 𝕩 } ⌾ ( 𝕩 ⊸ ⊑ ) 𝕨 }
# Recursive Bounce
Bounce ←{ ( w ‿ d ) S x : {
⊑ opaque ∊ ˜ ⊑ w ⊑ x ? ( 3 = ⊑ w ⊑ x ) ◶ ⟨ x , ⟨ 5 ⟩ ˙ ⌾ ( w ⊸ ⊑ ) x ⟩ @ ; # laser hits a non-mirror
⊑ empties ∊ ˜ ⊑ w ⊑ x ? # Empty space
⟨ w + d , d ⟩ S { ( ( × ⊑ d ) ⊑ ⟨ 9 ‿ 9 ‿ 1 1 ‿ 1 1 , 10 ‿ 1 1 ‿ 1 0 ‿ 1 1 ⟩ ) ⊏ ˜ 0 ‿ 9 ‿ 1 0 ‿ 1 1 ⊐ 𝕩 } ⌾ ( w ⊸ ⊑ ) x ; # Draw laser and recurse
#⟨w+d,d⟩S{((× ⊑d)⊑"--++"‿"|+|+")⊏˜" -|+"⊐𝕩}⌾(w⊸⊑)x; # Draw laser and recurse
# laser hits a mirror
d ←⌽ d × - ⊸ ¬ 7 = ⊑ w ⊑ x # calculate the mirror bounce direction
⟨ w + d , d ⟩ S x } # recurse to the next position
}
Shoot ←{ 𝕩 { 𝕨 Bounce ´ ⌽ 𝕩 } ∾ ⟨ < 0 ‿ ¯1 , < 0 ‿ 1 , < ¯1 ‿ 0 , < 1 ‿ 0 ⟩ ( ⊣ ⋈ ˜ ¨ + ) ¨ FindIdx ⟜ ( ⊑ ¨ 𝕩 ) ¨ 12 ‿ 1 3 ‿ 1 4 ‿ 1 5 }
Step ←{ Push ⌾ ( ( 𝕨 Tiles ⊑ 1 FindIdx ⊑ ¨ 𝕩 ) ⊸ ⊑ ) 𝕩 } # 𝕨 S 𝕩 | 𝕨 : direction | 𝕩 :level | Step the game
Draw ←{ ∾ ´ ¨ < ˘ { 𝕊 '⭍' : ansi .yellow ∾ 𝕩 ; ⊑ 𝕩 ∊ "|-+" ? ansi .red ∾ 𝕩 ; ⊑ 𝕩 ∊ "\/" ? ansi .cyan ∾ 𝕩 ; ansi .defaultB ∾ 𝕩 } ¨ chars ⊏ ˜ + ´ ¨ Shoot 𝕨 Step ´ ⌽ 𝕩 } # 𝕨 Draw 𝕩 | 𝕨 : levels | 𝕩 : moves | Draw the game in ASCII
Win ←{ ¬ ∨ ´ ∨ ˝ 3 = ⊑ ¨ Shoot 𝕩 } # W 𝕩 | 𝕩 : level | [W]in condition, no unpowered goals after shooting laser
Next ←{ moves ↩moves ∾ < 𝕩 }
Undo ←{ 𝕊 : moves ↩( - 1 < ≠ ) ⊸ ↓ moves }
# Main loop
levels ←Ascii2Matrix ¨ > ¨ SplitOnEmpty •FLines "levels" # Load file containing levels
currentLevel ←0
e ←ansi .e
•term .RawMode 1 # set terminal to raw mode
•Out e ∾ "[?25l" ∾ e ∾ "[2J" ∾ e ∾ "[H" # Cursor to origin, hide it and clear screen
clear ←""
{ 𝕤 # Loop until the user wins
•Out e ∾ "[H" # Cursor to origin
•Out "Level: " ∾ •Repr 1 + currentLevel
•Out e ∾ "7" # Save cursor position
•Out ansi .yellow ∾ "⭍" ∾ ansi .defaultB ∾ " Power the machines (⊕) by moving the mirrors (" ∾ ansi .cyan ∾ "\/" ∾ ansi .defaultB ∾ ") "
•Out "Controls: (hjkl or wasd) to move, u to undo, r to reset level, q to quit"
•Out ¨ ( currentLevel ⊑ levels ) Draw moves
key ←•term .CharB @
{ 𝕤 ⋄ Next ⊑ ( "hjkl" = key ) / ⟨ 0 ‿ ¯1 , 1 ‿ 0 , ¯1 ‿ 0 , 0 ‿ 1 ⟩ } ⍟ ( ⊑ key ∊ "hjkl" ) @
{ 𝕤 ⋄ Next ⊑ ( "aswd" = key ) / ⟨ 0 ‿ ¯1 , 1 ‿ 0 , ¯1 ‿ 0 , 0 ‿ 1 ⟩ } ⍟ ( ⊑ key ∊ "aswd" ) @
{ 𝕤 ⋄ Undo @ } ⍟ ( key = 'u' ) @
{ 𝕤 ⋄ •Out e ∾ "[?12l" ∾ e ∾ "[?25h" ⋄ •Exit 0 } ⍟ ( key = 'q' ) @
{ 𝕤 ⋄ moves ↩⟨ 0 ‿ 0 ⟩ } ⍟ ( key = 'r' ) @
{ 𝕤 ⋄ clear ↩e ∾ "[2J" } ⍟ ( ( ( 1 ⊸ + > ○ ( ⌊ 1 + 10 ⋆ ⁼ 1 ⌈ ⊢ ) ⊢ ) ¯1 + ≠ moves ) ∧ key = 'u' ) @
•Out e ∾ "8" # Restore cursor position
{ 𝕤 ⋄ •Out clear ∾ ansi .yellow ∾ "⭍" ∾ ansi .defaultB ∾ " Power the machines (⊕) by moving the mirrors (" ∾ ansi .cyan ∾ "\/" ∾ ansi .defaultB ∾ ") " ∾ ( @ + 10 ) ∾ "Controls: (hjkl or wasd) to move, u to undo, r to reset level, q to quit" } ⍟ ( ⊑ key ∊ "wasdhjkluqr" ) @
{ 𝕤 ⋄ •Out "Invalid key: (hjkl or wasd) to move, u to undo, r to reset level" } ⍟ ( ¬ ⊑ key ∊ "wasdhjkluqr" ) @
•Out ¨ ( currentLevel ⊑ levels ) Draw moves
•Out "Moves: " ∾ •Repr ¯1 + ≠ moves
{ 𝕤 ⋄ •Out "Good job!, press any key to continue to the next level" ⋄ •term .CharB @ ⋄ e ∾ "[H" ⋄ currentLevel ↩currentLevel + 1 ⋄ moves ↩⟨ 0 ‿ 0 ⟩ } ⍟ ( Win ( currentLevel ⊑ levels ) Step ´ ⌽ moves ) @
} •_While_ { 𝕤 ⋄ currentLevel < ≠ levels } @
•Out "Well played, you win!"
•Out e ∾ "[?12l" ∾ e ∾ "[?25h"