2023-03-04 18:38:17 +01:00
#!/usr/bin/env BQN
# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 Rampoina <rampoina@protonmail.com>
#
# Arc
2023-03-04 19:33:22 +01:00
# The level is a 2d matrix of lists (tiles)
# Each list contains the objects of the game represented as ints
2023-03-04 18:38:17 +01:00
moves ←⟨ 0 ‿ 0 ⟩ # list of moves, each move is a direction, we start without moving
2023-03-05 03:05:16 +01:00
chars ←" λ$⊕⭍#/\-|+<>^v" # legal characters
⟨ floor , player , box , machine , pmachine , wall , lmirror , rmirror , hbeam , vbeam , xbeam , llaser , rlaser , ulaser , dlaser ⟩ ←↕ ≠ chars
beams ←hbeam ‿ vbeam ‿ xbeam
mirrors ←lmirror ‿ rmirror
lasers ←llaser ‿ rlaser ‿ ulaser ‿ dlaser
movables ←player ‿ box ∾ mirrors # player, box and mirrors
opaque ←player ‿ box ‿ machine ‿ wall ∾ lasers # non laser reflecting
empties ←floor ∾ beams # floor and laser beams
2023-03-04 18:38:17 +01:00
ansi ←{
e ⇐@ + 27
red ⇐e ∾ "[31m"
cyan ⇐e ∾ "[36m"
yellow ⇐e ∾ "[33m"
defaultB ⇐e ∾ "[0m"
}
2023-03-05 03:05:16 +01:00
colors ←( ≠ chars ) ⥊ < ansi .defaultB # start with all glyphs being the default color
colors ansi .yellow ˙ ⌾ ( pmachine ⊸ ⊑ ) ↩
colors ansi .cyan ¨ ⌾ ( lmirror ‿ rmirror ⊸ ⊏ ) ↩
colors ansi .red ¨ ⌾ ( hbeam ‿ vbeam ‿ xbeam ⊸ ⊏ ) ↩
Colorize ←{ 𝕩 ∾ ˜ colors ⊑ ˜ ⊑ chars ⊐ 𝕩 } # 𝕩 : character | Turn character into color+character
lTiles ←{ ⊑ 𝕩 ∊ movables ? 𝕩 ‿ floor ; ≍ 𝕩 } ¨ ↕ ≠ chars # list of all the possible tiles as lists of numbers
2023-03-04 19:33:22 +01:00
# Turns the level from ascii strings into a 2d matrix of lists
2023-03-05 03:05:16 +01:00
Ascii2Matrix ←{ ( ⊑ chars ⊐ 𝕩 ) ⊑ lTiles } ¨ ( ⊢ ↑ ˝ · ≍ ⟜ ¬ 2 + ≢ )
2023-03-04 18:38:17 +01:00
# 𝕨 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⟩⟩
2023-03-04 18:50:07 +01:00
# Move the first object in the first tile to the second tile.
2023-03-04 18:38:17 +01:00
# 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)⟩
2023-03-04 18:50:07 +01:00
# Given 3 tiles try to [P]ush the second tile (possible box)
2023-03-04 18:38:17 +01:00
# and afterwards try to move the first one (player) if possible
Push ←Move ⌾ ( 2 ↑ ⊢ ) Move ⌾ ( 1 ↓ ⊢ )
FindIdx ←/ ○ ⥊ ⟜ ( ↕ ≢ ) ∘ ⍷
2023-03-04 19:33:22 +01:00
# w‿d Bounce x | x: map | w‿d: w: current position, d: direction of the laser
# Calculates the bounces of a laser beam recursively
2023-03-04 18:38:17 +01:00
Bounce ←{ ( w ‿ d ) S x : {
2023-03-05 03:05:16 +01:00
⊑ opaque ∊ ˜ ⊑ w ⊑ x ? ( machine = ⊑ w ⊑ x ) ◶ ⟨ x , ⟨ pmachine ⟩ ˙ ⌾ ( w ⊸ ⊑ ) x ⟩ @ ; # laser hits a non-mirror
2023-03-04 18:38:17 +01:00
⊑ empties ∊ ˜ ⊑ w ⊑ x ? # Empty space
2023-03-05 03:05:16 +01:00
⟨ w + d , d ⟩ S { ( ( × ⊑ d ) ⊑ ⟨ hbeam ‿ hbeam ‿ xbeam ‿ xbeam , vbeam ‿ xbeam ‿ vbeam ‿ xbeam ⟩ ) ⊏ ˜ floor ‿ hbeam ‿ vbeam ‿ xbeam ⊐ 𝕩 } ⌾ ( w ⊸ ⊑ ) x ; # Draw laser and recurse
2023-03-04 18:38:17 +01:00
# laser hits a mirror
2023-03-05 03:05:16 +01:00
d ←⌽ d × - ⊸ ¬ lmirror = ⊑ w ⊑ x # calculate the mirror bounce direction
2023-03-04 18:38:17 +01:00
⟨ w + d , d ⟩ S x } # recurse to the next position
}
2023-03-04 19:33:22 +01:00
# Shoot 𝕩 | 𝕩 : map | calculates the bounces for each laser
2023-03-05 03:05:16 +01:00
Shoot ←{ 𝕩 { 𝕨 Bounce ´ ⌽ 𝕩 } ∾ ⟨ < 0 ‿ ¯1 , < 0 ‿ 1 , < ¯1 ‿ 0 , < 1 ‿ 0 ⟩ ( ⊣ ⋈ ˜ ¨ + ) ¨ FindIdx ⟜ ( ⊑ ¨ 𝕩 ) ¨ lasers }
Step ←{ Push ⌾ ( ( 𝕨 Tiles ⊑ player FindIdx ⊑ ¨ 𝕩 ) ⊸ ⊑ ) 𝕩 } # 𝕨 S 𝕩 | 𝕨 : direction | 𝕩 :level | Step the game
2023-03-04 19:33:22 +01:00
Draw ←{ ∾ ´ ¨ < ˘ Colorize ¨ chars ⊏ ˜ + ´ ¨ Shoot 𝕨 Step ´ ⌽ 𝕩 } # 𝕨 Draw 𝕩 | 𝕨 : levels | 𝕩 : moves | Draw the game in ASCII
2023-03-04 18:38:17 +01:00
Win ←{ ¬ ∨ ´ ∨ ˝ 3 = ⊑ ¨ Shoot 𝕩 } # W 𝕩 | 𝕩 : level | [W]in condition, no unpowered goals after shooting laser
Next ←{ moves ↩moves ∾ < 𝕩 }
Undo ←{ 𝕊 : moves ↩( - 1 < ≠ ) ⊸ ↓ moves }
# Main loop
2023-03-04 19:33:22 +01:00
SplitOnEmpty ←{ 𝕩 ⊔ ˜ ( ⊢ - ˜ + ` × ¬ ) 0 = ≠ ¨ 𝕩 }
2023-03-04 18:38:17 +01:00
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
2023-03-04 19:33:22 +01:00
{ 𝕤 ⋄ •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" ) @
2023-03-04 18:38:17 +01:00
{ 𝕤 ⋄ •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
2023-03-04 19:13:53 +01:00
{ 𝕤
•Out "Good job!, press any key to continue to the next level"
•term .CharB @
•Out e ∾ "[H" ∾ e ∾ "[0J"
currentLevel ↩currentLevel + 1 ⋄ moves ↩⟨ 0 ‿ 0 ⟩
} ⍟ ( Win ( currentLevel ⊑ levels ) Step ´ ⌽ moves ) @
2023-03-04 18:38:17 +01:00
} •_While_ { 𝕤 ⋄ currentLevel < ≠ levels } @
•Out "Well played, you win!"
•Out e ∾ "[?12l" ∾ e ∾ "[?25h"