update PicoCalc SD

This commit is contained in:
cuu
2026-03-01 15:25:55 +08:00
parent c1b8f3fc3e
commit d5c0a8531a
101 changed files with 4913 additions and 58 deletions

Binary file not shown.

View File

@@ -0,0 +1,216 @@
# MachiKania system
# initialization file
# Specify the autoexec file
AUTOEXEC=MACHIKAP.BAS
# Set the direction of LCD
# HORIZONTAL # same as LCD0TURN
VERTICAL # same as LCD270TURN
# LCD180TURN
# LCD90TURN
# When using IPS-LCD, activate following line
LCDINVERT
# Setup Initial Text width (42 or 80 for Type PU)
# WIDTH42
# WIDTH80
# Decide if rotate button assignment when LCD vertical setting
ROTATEBUTTONS
# NOROTATEBUTTONS
# Decide if output to USB serial port
USBSERIALON
# USBSERIALOFF
# Decide if emulate buttons by keyboard by setting key codes
# Default:
# UP button: UP key
# DOWN button: DOWN key
# LEFT button: LEFT key
# RIGHT button: RIGHT key
# START button: Enter key
# FIRE button: Space key
EMULATEBUTTONUP=38
EMULATEBUTTONDOWN=40
EMULATEBUTTONLEFT=37
EMULATEBUTTONRIGHT=39
EMULATEBUTTONSTART=13
EMULATEBUTTONFIRE=32
# Decide if output to LCD
LCDOUTON
# LCDOUTOFF
# Decide if wait for 2.5 seconds in the beginning in debug mode
DEBUGWAIT2500
# NODEBUGWAIT2500
# Infinite loop or reset at the end of program
LOOPATEND
# RESETATEND
# Waiting time at the beginning in milli seconds (must be more than 499)
STARTWAIT=500
# What to do when an exception occurs
# EXCRESET # Reset Machikania (default: off)
# EXCSCREENSHOT=EXCSCRS.BIN # Save screen shot as a file (default: off)
# EXCDUMP=EXDUMP.BIN # Dump memory to a file (default: off)
# Waiting time for USB keyboard connection in milli seconds (0: infinite)
#WAIT4KEYBOARD=0
WAIT4KEYBOARD=2000
# Specify the keyboard type
# Enable the keyword below
# 106KEY # Japanese Keyboard
101KEY # US Keyboard
# Setup Lock keys' status
# Comment out if not lock
# the key when initializing
NUMLOCK # Num Lock Key
CAPSLOCK # Caps Lock Key
# SCRLLOCK # Scroll Lock Key
# Specify SPI pins
# Comment out for default settings
# Useful for small RP2040 boards like XIAO RP2040, RP2040-Zero, and Tiny-2040
# Valid values for SPIMISO: 0, 4, 16, 20 (SPI0)
# SPIMISO: 8, 12, 24, 28 (SPI1)
# Valid values for SPIMOSI: 3, 7, 19, 23 (SPI0)
# SPIMOSI: 11, 15, 27 (SPI1)
# Valid values for SPICLK: 2, 6, 18, 22 (SPI0)
# SPICLK: 10, 14, 26 (SPI1)
# SPIMISO=4
# SPIMOSI=7
# SPICLK=6
# Specify UART pins
# Comment out for default settings
# Valid values for UARTTX: 0, 12, 16, 28 (UART0)
# UARTTX: 4, 8, 20, 24 (UART1)
# Valid values for UARTRX: 1, 13, 17, 29 (UART0)
# UARTRX: 5, 9, 21, 25 (UART1)
# UARTTX=4
# UARTRX=5
# Specify I2C pins
# Comment out for default settings
# Note that I2C1 cannot be used for PicoCalc as it is used for keyboard
# Valid values for I2CSDA: 0, 4, 8, 12, 16, 20, 24, 28 (I2C0)
# I2CSDA: 2, 6, 10, 14, 18, 22, 26 (I2C1)
# Valid values for I2CSCL: 1, 5, 9, 13, 17, 21, 25, 29 (I2C0)
# I2CSCL: 3, 7, 11, 15, 19, 23, 27 (I2C1)
# I2CSDA=0
# I2CSCL=1
# Use Real Time Clock (RTC) for saving files by file system
# If RTC is not use, timestamp of save file will be 2020/01/01
# Remove comment to enable RTC for file system in following line
# RTCFILE
# Timezone used for RTC setting by NTP
# TIMEZONE=9 # JST
# TIMEZONE=8.5 # ACST
# TIMEZONE=5.75 # Nepal Standard Time
TIMEZONE=0 # UTC
# TIMEZONE=-8 # PST
# TIMEZONE=-7 # PDT
# TIMEZONE=-5 # EST
# TIMEZONE=-4 # EDT
# Connect to wifi when starting (Pico W only)
# USEWIFI
# Set country code for wifi connection (Pico W only)
WIFICOUNTRY=JP
# WIFICOUNTRY=US
# Define wifi SSID and password (Pico W only)
# WIFISSID=xxxx
# WIFIPASSWD=xxxx
# Define hostname (Pico W only; default: PicoW)
# HOSTNAME=machikania
# Define NTP server (Pico W only)
NTPSERVER=pool.ntp.org
# Enable following line to connect to NTP server in the beginning after power on (Pico W only)
# Make sure that USEWIFI is enabled when using this feature
# INITIALNTP
# Enable following line to show timestamp when selecting files
# SHOWTIMESTAMP
# Sort order when selecting files
# 0:A...Z 1:Z...A 2:OLD...NEW 3:NEW...OLD
FILESORTBY=0
# Set help file used in editor
# Use help-e.txt for English help
# Use help-k.txt for Japanese Kana help
HELPFILE=/docs/help-e.txt
#HELPFILE=/docs/help-k.txt
# Assign the additional PWM (4-9) to GPIO number
# Note that this is not MachiKania port number, but RP2040/2350 GPIO number (0-29)
# PWM4=21

View File

@@ -1,75 +1,209 @@
# PicoCalc SD
# PicoCalc SD v0.6
This repository contains the official factory image and files for the **PicoCalc SD card**. It includes essential firmware, applications, and system files required for the proper functioning of **PicoCalc**.
## Directory Overview
```
PicoCalc SD/
├── BellLabs_Fine.mp3
├── bifdiag.bas
├── BOOT2040.uf2
├── bootloader_pico.uf2
├── cc
│   ├── edit.lua
│   ├── expect.lua
│   ├── internal
│   │   ├── menu.lua
│   │   └── syntax
│   │   ├── errors.lua
│   │   ├── init.lua
│   │   ├── lexer.lua
│   │   └── parser.lua
│   └── pretty.lua
├── Chessnovice_johnybot.nes
├── firmware
│   ├── PicoCalc_Fuzix_v1.0.img
├── fonts
│   ├── 6x10.fnt
│   ├── Acer8x8.fnt
│   ├── Haxor12.fnt
│   ├── HP6x8.fnt
│   ├── HP8x8.fnt
│   └── ProggyClean.fnt
├── lorenz.bas
├── lua
│   ├── asteroids.lua
│   ├── boxworld.bmp
│   ├── boxworld.lua
│   ├── browser.lua
│   ├── bubble.lua
│   ├── mandelbrot.lua
│   └── piano.lua
├── MACHIKAP.INI
├── main.lua
├── mand.bas
├── pico1-apps
│   ├── MicroPython_fa8b24c.uf2
│   ├── phyllosoma_kb.uf2
│   ├── PicoCalc_Fuzix_v1.0.uf2
│   ├── PicoCalc_MP3Player_v0.5.uf2
│   ├── PicoCalc_NES_v1.0.uf2
│   ├── PicoCalc_PicoMite_v1.0.uf2
│   ├── PicoCalc_stm32 #PicoCalc keyboard firmware
│   │   ├── PicoCalc_firmware_v1.0.bin
│   │   └── PicoCalc_firmware_v1.0.hex
│   └── PicoCalc_uLisp_v1.0.uf2
├── lorenz.bas
├── mand.bas
│   ├── PicoCalc_uLisp_v1.1.uf2
│   ├── picolua_daf20a2.uf2
│   ├── PicoMite_v6.02.01b4_beta.uf2
│   └── Picoware_v1.6.9.uf2
├── picocalc.bmp
├── picoware
│   ├── apps
│   │   ├── Calculator.mpy
│   │   ├── cat-fact.mpy
│   │   ├── counter.mpy
│   │   ├── flip_social
│   │   │   ├── __init__.py
│   │   │   ├── password.mpy
│   │   │   ├── run.mpy
│   │   │   ├── settings.mpy
│   │   │   └── username.mpy
│   │   ├── FlipSocial.mpy
│   │   ├── games
│   │   │   ├── 2048.mpy
│   │   │   ├── Breakout.mpy
│   │   │   ├── example.mpy
│   │   │   ├── Flappy Bird.mpy
│   │   │   ├── flip_world
│   │   │   │   ├── assets.mpy
│   │   │   │   ├── general.mpy
│   │   │   │   ├── __init__.py
│   │   │   │   ├── player.mpy
│   │   │   │   ├── run.mpy
│   │   │   │   └── sprite.mpy
│   │   │   ├── FlipWorld.mpy
│   │   │   ├── free_roam
│   │   │   │   ├── dynamic_map.mpy
│   │   │   │   ├── game.mpy
│   │   │   │   ├── __init__.py
│   │   │   │   ├── maps.mpy
│   │   │   │   ├── player.mpy
│   │   │   │   └── sprite.mpy
│   │   │   ├── Free Roam.mpy
│   │   │   ├── game_of_life.mpy
│   │   │   ├── Maze Runner.mpy
│   │   │   ├── Minesweeper.mpy
│   │   │   ├── Pong.mpy
│   │   │   ├── Snake.mpy
│   │   │   ├── Soduko.mpy
│   │   │   ├── Space Invaders.mpy
│   │   │   ├── Tetris.mpy
│   │   │   └── Tower Defense.mpy
│   │   ├── Graph.mpy
│   │   ├── hello_color.mpy
│   │   ├── keyboard-simple.mpy
│   │   ├── loading-simple.mpy
│   │   ├── menu-simple.mpy
│   │   ├── random-object.mpy
│   │   ├── screensavers
│   │   │   ├── Bubble Universe.mpy
│   │   │   ├── Clock.mpy
│   │   │   ├── Cube.mpy
│   │   │   ├── DVI Bounce.mpy
│   │   │   ├── Fire Effect.mpy
│   │   │   ├── Matrix Rain.mpy
│   │   │   ├── Patterns.mpy
│   │   │   ├── PicoFlower.mpy
│   │   │   ├── Plasma Wave.mpy
│   │   │   └── Yin-Yang.mpy
│   │   ├── Serial Terminal.mpy
│   │   ├── storage-simple.mpy
│   │   ├── textbox-simple.mpy
│   │   ├── Text Editor.mpy
│   │   └── Weather.mpy
│   └── settings
└── README.md
Since PicoCalc SD v0.6 we used [uf2loader](https://github.com/pelrun/uf2loader.git) as main loader.
All uf2 files in folder **pico1-apps** will be showed up in menu.
Once a uf2 got flashed and ran, next time you can use menu item **[Default App]** to directly run it without flashing it again.
Important files:
* bootloader_pico.uf2
uf2loader main program, should be flashed into pico.
* BOOT2040.uf2
uf2loader Menu UI, it is a very important file, do not delete or edit it unless you know what you are doing.
## Path: pico1-apps/PicoCalc_Fuzix_v1.0.uf2 (Download)
[Fuzix](https://github.com/EtchedPixels/FUZIX.git) is an open-source, lightweight Unix-like operating system specifically designed for 8-bit and other resource-constrained processors.
Patches for PicoCalc are [here](https://github.com/clockworkpi/PicoCalc/tree/master/Code/FUZIX)
## Path: pico1-apps/PicoCalc_NES_v1.0.uf2 (Download)
A simple NES emulator for PicoCalc, it will scan all nes files in the root of SD card.
Given the resource constraints of the Pico, it is recommended to only run NES games that are less than **44KB** in size.
## Path: pico1-apps/picolua_daf20a2.uf2 (Download)
https://github.com/Lana-chan/picocalc_lua
A Lua interpreter for PicoCalc. It contains a REPL, basic API to draw graphics, read keys and access the SD filesystem.
## Path: pico1-apps/Picoware_v1.6.9.uf2 (Download)
https://github.com/jblanked/Picoware
An Open-source custom firmware for the PicoCalc, Video Game Module, and other Raspberry Pi Pico devices.
Here is the version based on MicroPython.
## Path: pico1-apps/phyllosoma_kb.uf2 (Download)
[MachiKania Phyllosoma](https://github.com/machikania/phyllosoma/releases) is a BASIC compiler for ARMv6-M with excellent performance., especially for Raspberry Pi Pico.
## Path: pico1-apps/PicoCalc_MP3Player_v0.5.uf2 (Download)
[PicoCalc_MP3Player](https://github.com/clockworkpi/PicoCalc/tree/master/Code/MP3Player) is a simple MP3 playback program based on the [YAHAL](https://git.fh-aachen.de/Terstegge/YAHAL
) framework.
## Path: pico1-apps/PicoCalc_uLisp_v1.1.uf2 (Download)
http://www.ulisp.com/show?56ZO
A self-contained Lisp computer for PicoCalc.
## Path: pico1-apps/PicoMite_v6.02.01b4_beta.uf2 (Download)
[The PicoMite](https://geoffg.net/picomite.html) is a complete operating system with a Microsoft BASIC compatible interpreter and extensive hardware support including touch sensitive LCD panels, SD Cards, WiFi/Internet and much more.
```
Copyright and Acknowledgments
The PicoMite firmware and MMBasic is copyright 2011-2025 by Geoff Graham and Peter Mather 2016-2025.
1-Wire Support is copyright 1999-2006 Dallas Semiconductor Corporation and 2012 Gerard Sexton.
FatFs (SD Card) driver is copyright 2014, ChaN.
WAV, MP3, and FLAC file support is copyright 2019 David Reid.
JPG support is thanks to Rich Geldreich
The pico-sdk is copyright 2021 Raspberry Pi (Trading) Ltd.
TinyUSB is copyright tinyusb.org
LittleFS is copyright Christopher Haster
Thomas Williams and Gerry Allardice for MMBasic enhancements
The VGA driver code was derived from work by Miroslav Nemecek
The CRC calculations are copyright Rob Tillaart
The compiled object code (the .uf2 file) for the PicoMite firmware is free software: you can use or redistribute
it as you please. The source code is on GitHub ( https://github.com/UKTailwind/PicoMiteAllVersions ) and
can be freely used subject to some conditions (see the header in the source files).
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY, without even
the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
## Flashing the Factory SD Image
To restore your **PicoCalc SD card** to its factory state, follow these steps:
## Path: pico1-apps/MicroPython_fa8b24c.uf2 (Download)
### Requirements:
- A microSD card (at least **16GB** recommended)
- A computer with **Linux/macOS/Windows**
- A microSD card reader
https://github.com/zenodante/PicoCalc-micropython-driver
## Custom Partitioning
[MicroPython](https://micropython.org/) is a lean and efficient implementation of the Python 3 programming language that includes a small subset of the Python standard library and is optimised to run on microcontrollers and in constrained environments.
The **PicoCalc SD card** uses a dual-partition structure:
Here is the MicroPython Drivers compatible with PicoCalc.
| Partition | Size | Format | Purpose |
|-----------|--------|--------|---------|
| `/dev/sdX1` | Remaining space | **FAT32** | Storage for PicoMite, uLisp, NES Emulator, etc. |
| `/dev/sdX2` | **32MB** | **fuzix filesystem** | Root filesystem for **FUZIX** |
### Manually Partitioning an SD Card
If you need to manually create an SD card for **PicoCalc**, use the provided **partitioning script**:
```bash
wget https://github.com/clockworkpi/PicoCalc/raw/refs/heads/master/Code/scripts/partition_usb_32mb.sh
chmod +x partition_usb_32mb.sh
sudo ./partition_usb_32mb.sh sdb
```
*(Replace `sdb` with your actual SD card device.)*
### Flashing the FUZIX 32MB Image
- Download the FUZIX image:
[PicoCalc_Fuzix_v1.0.img](https://github.com/clockworkpi/PicoCalc/blob/master/Bin/PicoCalc%20SD/firmware/PicoCalc_Fuzix_v1.0.img)
- Flash the image to the second partition:
```bash
sudo dd if=filesystem.img of=/dev/sdb2
```
Please check the wiki for details
https://github.com/clockworkpi/PicoCalc/wiki/How-to-Create-an-Official-PicoCalc-SD-Card
## Notes
- The **USB Type-C port** is the default **serial port** for **PicoCalc**, not the Micro USB port.
- FUZIX supports a maximum **32MB** root filesystem.
- Ensure you backup your data before modifying the SD card.
---
For more details, visit the official **[PicoCalc GitHub Repository](https://github.com/clockworkpi/PicoCalc)**.

Binary file not shown.

815
Bin/PicoCalc SD/cc/edit.lua Normal file
View File

@@ -0,0 +1,815 @@
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
--
-- SPDX-License-Identifier: LicenseRef-CCPL
-- Get file to edit
local tArgs = { ... }
if #tArgs == 0 then
local programName = "edit"
print("Usage: " .. programName .. " <path>")
return
end
-- Error checking
local sPath = tArgs[1]
local bReadOnly = false
if fs.exists(sPath) and fs.isDir(sPath) then
print("Cannot edit a directory.")
return
end
local x, y = 1, 1
local w, h = term.getSize()
local scrollX, scrollY = 0, 0
local tLines, tLineLexStates = {}, {}
local bRunning = true
-- colors
local isColor = true
local highlightColor, keywordColor, textColor, bgColor, errorColor
if isColor then
bgColor = colors.black
textColor = colors.white
highlightColor = colors.yellow
keywordColor = colors.yellow
errorColor = colors.red
else
bgColor = colors.black
textColor = colors.white
highlightColor = colors.white
keywordColor = colors.white
errorColor = colors.white
end
-- Menus
local menu = require "cc.internal.menu"
local current_menu
local menu_items = {}
if not bReadOnly then
table.insert(menu_items, "Save")
end
--[[if shell.openTab then
table.insert(menu_items, "Run")
end
if peripheral.find("printer") then
table.insert(menu_items, "Print")
end]]
table.insert(menu_items, "Run")
table.insert(menu_items, "Exit")
local status_ok, status_text
local function set_status(text, ok)
status_ok = ok ~= false
status_text = text
end
if bReadOnly then
set_status("File is read only", false)
elseif fs.getFreeSpace(sPath) < 1024 then
set_status("Disk is low on space", false)
else
local message
message = "Press Ctrl to access menu"
if #message > w - 5 then
message = "Press Ctrl for menu"
end
set_status(message)
end
local function load(_sPath)
tLines = {}
if fs.exists(_sPath) then
bReadOnly = fs.isReadOnly(_sPath)
local file = fs.open(_sPath, "r")
local sLine = file:readLine()
while sLine do
table.insert(tLines, sLine)
table.insert(tLineLexStates, false)
sLine = file:readLine()
end
file:close()
end
if #tLines == 0 then
table.insert(tLines, "")
table.insert(tLineLexStates, false)
end
end
local function save(_sPath, fWrite)
-- Create intervening folder
--[[local sDir = _sPath:sub(1, _sPath:len() - fs.getName(_sPath):len())
if not fs.exists(sDir) then
fs.makeDir(sDir)
end]]
-- Save
local file, fileerr
local function innerSave()
file, fileerr = fs.open(_sPath, "w")
if file then
if file then
fWrite(file)
end
else
error("Failed to open " .. _sPath)
end
end
local ok, err = pcall(innerSave)
if file then
file:close()
end
return ok, err, fileerr
end
local tokens = require "cc.internal.syntax.parser".tokens
local lex_one = require "cc.internal.syntax.lexer".lex_one
local token_colors = {
[tokens.STRING] = colors.red,
[tokens.COMMENT] = colors.green,
[tokens.NUMBER] = colors.magenta,
-- Keywords
[tokens.AND] = keywordColor,
[tokens.BREAK] = keywordColor,
[tokens.DO] = keywordColor,
[tokens.ELSE] = keywordColor,
[tokens.ELSEIF] = keywordColor,
[tokens.END] = keywordColor,
[tokens.FALSE] = keywordColor,
[tokens.FOR] = keywordColor,
[tokens.FUNCTION] = keywordColor,
[tokens.GOTO] = keywordColor,
[tokens.IF] = keywordColor,
[tokens.IN] = keywordColor,
[tokens.LOCAL] = keywordColor,
[tokens.NIL] = keywordColor,
[tokens.NOT] = keywordColor,
[tokens.OR] = keywordColor,
[tokens.REPEAT] = keywordColor,
[tokens.RETURN] = keywordColor,
[tokens.THEN] = keywordColor,
[tokens.TRUE] = keywordColor,
[tokens.UNTIL] = keywordColor,
[tokens.WHILE] = keywordColor,
}
-- Fill in the remaining tokens.
for _, token in pairs(tokens) do
if not token_colors[token] then token_colors[token] = textColor end
end
local lex_context = { line = function() end, report = function() end }
local tCompletions
local nCompletion
local tCompleteEnv = _ENV
local function complete(sLine)
--[[if settings.get("edit.autocomplete") then
local nStartPos = string.find(sLine, "[a-zA-Z0-9_%.:]+$")
if nStartPos then
sLine = string.sub(sLine, nStartPos)
end
if #sLine > 0 then
return textutils.complete(sLine, tCompleteEnv)
end
end
return nil]]
end
local function recomplete()
local sLine = tLines[y]
if not bReadOnly and x == #sLine + 1 then
tCompletions = complete(sLine)
if tCompletions and #tCompletions > 0 then
nCompletion = 1
else
nCompletion = nil
end
else
tCompletions = nil
nCompletion = nil
end
end
local function writeCompletion(sLine)
if nCompletion then
local sCompletion = tCompletions[nCompletion]
term.setTextColor(colors.white)
term.setBackgroundColor(colors.grey)
term.write(sCompletion)
term.setTextColor(textColor)
term.setBackgroundColor(bgColor)
end
end
--- Check if two values are equal. If both values are lists, then the contents will be
-- checked for equality, to a depth of 1.
--
-- @param x The first value.
-- @param x The second value.
-- @treturn boolean Whether the values are equal.
local function shallowEqual(x, y)
if x == y then return true end
if type(x) ~= "table" or type(y) ~= "table" then return false end
if #x ~= #y then return false end
for i = 1, #x do if x[i] ~= y[i] then return false end end
return true
end
local function redrawLines(line, endLine)
term.setCursorBlink(false)
if not endLine then endLine = line end
local color = term.getTextColor()
-- Highlight all lines between line and endLine, highlighting further lines if their
-- lexer state has changed and aborting at the end of the screen.
local changed = false
while (changed or line <= endLine) and line - scrollY < h do
term.setCursorPos(1 - scrollX, line - scrollY)
term.clearLine()
local contents = tLines[line]
if not contents then break end
-- Lex our first token, either taking our continuation state (if present) or
-- the default lexer.
local pos, token, _, finish, continuation = 1
local lex_state = tLineLexStates[line]
if lex_state then
token, finish, _, continuation = lex_state[1](lex_context, contents, table.unpack(lex_state, 2))
else
token, _, finish, _, continuation = lex_one(lex_context, contents, 1)
end
while token do
-- start at scrollX
if finish >= scrollX+1 then
-- Print out that token
local new_color = token_colors[token]
if new_color ~= color then
term.setTextColor(new_color)
color = new_color
end
-- limit printed line to screen
if pos < scrollX+1 then pos = scrollX+1 end
local cap = finish < scrollX+w and finish or scrollX+w
term.write(contents:sub(pos, cap))
end
pos = finish + 1
-- end if we're past the screen width
if pos > scrollX+w then break end
-- If we have a continuation, then we've reached the end of the line. Abort.
if continuation then break end
-- Otherwise lex another token and continue.
token, _, finish, _, continuation = lex_one(lex_context, contents, pos)
end
-- Print the rest of the line. We don't strictly speaking need this, as it will
-- only ever contain whitespace.
term.write(contents:sub(pos))
if line == y and x == #contents + 1 then
writeCompletion()
color = term.getTextColor()
end
line = line + 1
-- Update the lext state of the next line. If that has changed, then
-- re-highlight it too. We store the continuation as nil rather than
-- false, to ensure we use the array part of the table.
if continuation == nil then continuation = false end
if tLineLexStates[line] ~= nil and not shallowEqual(tLineLexStates[line], continuation) then
tLineLexStates[line] = continuation or false
changed = true
else
changed = false
end
end
term.setTextColor(colors.white)
term.setCursorPos(x - scrollX, y - scrollY)
end
local function redrawText()
redrawLines(scrollY + 1, scrollY + h - 1)
end
local function redrawMenu()
term.setCursorBlink(false)
-- Clear line
term.setCursorPos(1, h)
term.clearLine()
term.setCursorPos(1, h)
if current_menu then
-- Draw menu
menu.draw(current_menu)
else
-- Draw status
term.setTextColor(status_ok and highlightColor or errorColor)
term.write(status_text)
term.setTextColor(textColor)
-- Draw line numbers
term.setCursorPos(w - #("Ln " .. y) + 1, h)
term.setTextColor(highlightColor)
term.write("Ln ")
term.setTextColor(textColor)
term.write(y)
end
-- Reset cursor
term.setCursorPos(x - scrollX, y - scrollY)
term.setCursorBlink(not current_menu)
end
local tMenuFuncs = {
Save = function()
if bReadOnly then
set_status("Access denied", false)
else
local ok, _, fileerr = save(sPath, function(file)
for _, sLine in ipairs(tLines) do
file:writeLine(sLine)
end
end)
if ok then
set_status("Saved to " .. sPath)
else
if fileerr then
set_status("Error saving: " .. fileerr, false)
else
set_status("Error saving to " .. sPath, false)
end
end
end
redrawMenu()
end,
--[[Print = function()
local printer = peripheral.find("printer")
if not printer then
set_status("No printer attached", false)
return
end
local nPage = 0
local sName = fs.getName(sPath)
if printer.getInkLevel() < 1 then
set_status("Printer out of ink", false)
return
elseif printer.getPaperLevel() < 1 then
set_status("Printer out of paper", false)
return
end
local screenTerminal = term.current()
local printerTerminal = {
getCursorPos = printer.getCursorPos,
setCursorPos = printer.setCursorPos,
getSize = printer.getPageSize,
write = printer.write,
}
printerTerminal.scroll = function()
if nPage == 1 then
printer.setPageTitle(sName .. " (page " .. nPage .. ")")
end
while not printer.newPage() do
if printer.getInkLevel() < 1 then
set_status("Printer out of ink, please refill", false)
elseif printer.getPaperLevel() < 1 then
set_status("Printer out of paper, please refill", false)
else
set_status("Printer output tray full, please empty", false)
end
term.redirect(screenTerminal)
redrawMenu()
term.redirect(printerTerminal)
sleep(0.5)
end
nPage = nPage + 1
if nPage == 1 then
printer.setPageTitle(sName)
else
printer.setPageTitle(sName .. " (page " .. nPage .. ")")
end
end
local old_menu = current_menu
current_menu = nil
term.redirect(printerTerminal)
local ok, error = pcall(function()
term.scroll()
for _, sLine in ipairs(tLines) do
print(sLine)
end
end)
term.redirect(screenTerminal)
if not ok then
print(error)
end
while not printer.endPage() do
set_status("Printer output tray full, please empty")
redrawMenu()
sleep(0.5)
end
current_menu = old_menu
if nPage > 1 then
set_status("Printed " .. nPage .. " Pages")
else
set_status("Printed 1 Page")
end
redrawMenu()
end,]]
Exit = function()
bRunning = false
end,
Run = function()
local sTempPath = "~temp.lua"
--[[if fs.exists(sTempPath) then
set_status("Error saving to " .. sTempPath, false)
return
end]]
local ok = save(sTempPath, function(file)
for _, sLine in ipairs(tLines) do
file:writeLine(sLine)
end
end)
if ok then
collectgarbage()
local f, err = loadfile(sTempPath)
if not f then
set_status(tostring(err))
else
term.clear()
keys.flush()
print("Free memory: "..sys.freeMemory())
set_status("Press Ctrl to access menu")
local status, err = pcall(f)
draw.enableBuffer(false)
if not status then print(tostring(err)) end
term.setCursorPos(1, h)
term.write("Press any key...")
keys.flush()
keys.wait(false, true)
end
--fs.delete(sTempPath)
else
set_status("Error saving to " .. sTempPath, false)
end
collectgarbage()
term.clear()
redrawMenu()
redrawText()
end,
}
local function setCursor(newX, newY)
local _, oldY = x, y
x, y = newX, newY
local screenX = x - scrollX
local screenY = y - scrollY
local bRedraw = false
if screenX < 1 then
scrollX = x - 1
screenX = 1
bRedraw = true
elseif screenX > w then
scrollX = x - w
screenX = w
bRedraw = true
end
if screenY < 1 then
scrollY = y - 1
screenY = 1
bRedraw = true
elseif screenY > h - 1 then
scrollY = y - (h - 1)
screenY = h - 1
bRedraw = true
end
recomplete()
if bRedraw then
redrawText()
elseif y ~= oldY then
redrawLines(math.min(y, oldY), math.max(y, oldY))
else
redrawLines(y)
end
term.setCursorBlink(not current_menu);
redrawMenu()
end
-- Actual program functionality begins
load(sPath)
term.setBackgroundColor(bgColor)
term.clear()
term.setCursorPos(x, y)
recomplete()
redrawText()
redrawMenu()
local function acceptCompletion()
if nCompletion then
-- Append the completion
local sCompletion = tCompletions[nCompletion]
tLines[y] = tLines[y] .. sCompletion
setCursor(x + #sCompletion, y)
end
end
local function handleMenuEvent(key)
assert(current_menu)
local result = menu.handle_event(current_menu, key)
if result == false then
current_menu = nil
redrawMenu()
elseif result ~= nil then
tMenuFuncs[result]()
current_menu = nil
redrawMenu()
end
end
-- Handle input
while bRunning do
term.setCursorBlink(not current_menu)
state, modifiers, key = keys.wait()
if state == keys.states.pressed or state == keys.states.longHold then
if current_menu then
handleMenuEvent(key)
else
if key == keys.up then
if nCompletion then
-- Cycle completions
nCompletion = nCompletion - 1
if nCompletion < 1 then
nCompletion = #tCompletions
end
redrawLines(y)
elseif y > 1 then
-- Move cursor up
setCursor(
math.min(x, #tLines[y - 1] + 1),
y - 1
)
end
elseif key == keys.down then
if nCompletion then
-- Cycle completions
nCompletion = nCompletion + 1
if nCompletion > #tCompletions then
nCompletion = 1
end
redrawLines(y)
elseif y < #tLines then
-- Move cursor down
setCursor(
math.min(x, #tLines[y + 1] + 1),
y + 1
)
end
elseif key == keys.tab and not bReadOnly then
if nCompletion and x == #tLines[y] + 1 then
-- Accept autocomplete
acceptCompletion()
else
-- Indent line
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. "\t" .. string.sub(sLine, x)
setCursor(x + 1, y)
end
elseif key == keys.pageUp then
-- Move up a page
local newY
if y - (h - 1) >= 1 then
newY = y - (h - 1)
else
newY = 1
end
setCursor(
math.min(x, #tLines[newY] + 1),
newY
)
elseif key == keys.pageDown then
-- Move down a page
local newY
if y + (h - 1) <= #tLines then
newY = y + (h - 1)
else
newY = #tLines
end
local newX = math.min(x, #tLines[newY] + 1)
setCursor(newX, newY)
elseif key == keys.home then
-- Move cursor to the beginning
if x > 1 then
setCursor(1, y)
end
elseif key == keys["end"] then
-- Move cursor to the end
local nLimit = #tLines[y] + 1
if x < nLimit then
setCursor(nLimit, y)
end
elseif key == keys.left then
if x > 1 then
-- Move cursor left
setCursor(x - 1, y)
elseif x == 1 and y > 1 then
setCursor(#tLines[y - 1] + 1, y - 1)
end
elseif key == keys.right then
local nLimit = #tLines[y] + 1
if x < nLimit then
-- Move cursor right
setCursor(x + 1, y)
elseif nCompletion and x == #tLines[y] + 1 then
-- Accept autocomplete
acceptCompletion()
elseif x == nLimit and y < #tLines then
-- Go to next line
setCursor(1, y + 1)
end
elseif key == keys.delete and not bReadOnly then
local nLimit = #tLines[y] + 1
if x < nLimit then
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. string.sub(sLine, x + 1)
recomplete()
redrawLines(y)
elseif y < #tLines then
tLines[y] = tLines[y] .. tLines[y + 1]
table.remove(tLines, y + 1)
table.remove(tLineLexStates, y + 1)
recomplete()
redrawText()
end
elseif key == keys.backspace and not bReadOnly then
if x > 1 then
-- Remove character
local sLine = tLines[y]
if x > 4 and string.sub(sLine, x - 4, x - 1) == " " and not string.sub(sLine, 1, x - 1):find("%S") then
tLines[y] = string.sub(sLine, 1, x - 5) .. string.sub(sLine, x)
setCursor(x - 4, y)
else
tLines[y] = string.sub(sLine, 1, x - 2) .. string.sub(sLine, x)
setCursor(x - 1, y)
end
elseif y > 1 then
-- Remove newline
local sPrevLen = #tLines[y - 1]
tLines[y - 1] = tLines[y - 1] .. tLines[y]
table.remove(tLines, y)
table.remove(tLineLexStates, y)
setCursor(sPrevLen + 1, y - 1)
redrawText()
end
elseif (key == keys.enter) and not bReadOnly then
-- Newline
local sLine = tLines[y]
local _, spaces = string.find(sLine, "^[ ]+")
if not spaces then
spaces = 0
end
tLines[y] = string.sub(sLine, 1, x - 1)
table.insert(tLines, y + 1, string.rep(' ', spaces) .. string.sub(sLine, x))
table.insert(tLineLexStates, y + 1, false)
setCursor(spaces + 1, y + 1)
redrawText()
elseif key == keys.control then
current_menu = menu.create(menu_items)
redrawMenu()
else
if keys.isPrintable(key) and not bReadOnly then
-- Input text
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. key .. string.sub(sLine, x)
setCursor(x + 1, y)
end
end
end
end
--[[elseif event[1] == "paste" and not bReadOnly then
-- Close menu if open
if current_menu then
current_menu = nil
redrawMenu()
end
-- Input text
local text = event[2]
local sLine = tLines[y]
tLines[y] = string.sub(sLine, 1, x - 1) .. text .. string.sub(sLine, x)
setCursor(x + #text, y)
elseif event[1] == "mouse_click" then
local button, cx, cy = event[2], event[3], event[4]
if current_menu then
handleMenuEvent(event)
else
if button == 1 then
-- Left click
if cy < h then
local newY = math.min(math.max(scrollY + cy, 1), #tLines)
local newX = math.min(math.max(scrollX + cx, 1), #tLines[newY] + 1)
setCursor(newX, newY)
else
current_menu = menu.create(menu_items)
redrawMenu()
end
end
end
elseif event[1] == "mouse_scroll" then
if not current_menu then
local direction = event[2]
if direction == -1 then
-- Scroll up
if scrollY > 0 then
-- Move cursor up
scrollY = scrollY - 1
redrawText()
end
elseif direction == 1 then
-- Scroll down
local nMaxScroll = #tLines - (h - 1)
if scrollY < nMaxScroll then
-- Move cursor down
scrollY = scrollY + 1
redrawText()
end
end
end
elseif event[1] == "term_resize" then
w, h = term.getSize()
setCursor(x, y)
redrawMenu()
redrawText()
end]]
end
-- Cleanup
function unrequire(m)
package.loaded[m] = nil
_G[m] = nil
end
unrequire("cc.internal.syntax.errors")
unrequire("cc.internal.syntax.lexer")
unrequire("cc.internal.syntax.parser")
unrequire("cc.internal.menu")
unrequire("cc.pretty")
unrequire("cc.expect")
collectgarbage()
term.clear()
term.setCursorBlink(false)
term.setCursorPos(1, 1)

View File

@@ -0,0 +1,145 @@
-- SPDX-FileCopyrightText: 2019 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- The [`cc.expect`] library provides helper functions for verifying that
function arguments are well-formed and of the correct type.
@module cc.expect
@since 1.84.0
@changed 1.96.0 The module can now be called directly as a function, which wraps around `expect.expect`.
@usage Define a basic function and check it has the correct arguments.
local expect = require "cc.expect"
local expect, field = expect.expect, expect.field
local function add_person(name, info)
expect(1, name, "string")
expect(2, info, "table", "nil")
if info then
print("Got age=", field(info, "age", "number"))
print("Got gender=", field(info, "gender", "string", "nil"))
end
end
add_person("Anastazja") -- `info' is optional
add_person("Kion", { age = 23 }) -- `gender' is optional
add_person("Caoimhin", { age = 23, gender = true }) -- error!
]]
local native_select, native_type = select, type
local function get_type_names(...)
local types = table.pack(...)
for i = types.n, 1, -1 do
if types[i] == "nil" then table.remove(types, i) end
end
if #types <= 1 then
return tostring(...)
else
return table.concat(types, ", ", 1, #types - 1) .. " or " .. types[#types]
end
end
local function get_display_type(value, t)
-- Lua is somewhat inconsistent in whether it obeys __name just for values which
-- have a per-instance metatable (so tables/userdata) or for everything. We follow
-- Cobalt and only read the metatable for tables/userdata.
if t ~= "table" and t ~= "userdata" then return t end
local metatable = debug.getmetatable(value)
if not metatable then return t end
local name = rawget(metatable, "__name")
if type(name) == "string" then return name else return t end
end
--- Expect an argument to have a specific type.
--
-- @tparam number index The 1-based argument index.
-- @param value The argument's value.
-- @tparam string ... The allowed types of the argument.
-- @return The given `value`.
-- @throws If the value is not one of the allowed types.
local function expect(index, value, ...)
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return value end
end
-- If we can determine the function name with a high level of confidence, try to include it.
local name
local ok, info = pcall(debug.getinfo, 3, "nS")
if ok and info.name and info.name ~= "" and info.what ~= "C" then name = info.name end
t = get_display_type(value, t)
local type_names = get_type_names(...)
if name then
error(("bad argument #%d to '%s' (%s expected, got %s)"):format(index, name, type_names, t), 3)
else
error(("bad argument #%d (%s expected, got %s)"):format(index, type_names, t), 3)
end
end
--- Expect an field to have a specific type.
--
-- @tparam table tbl The table to index.
-- @tparam string index The field name to check.
-- @tparam string ... The allowed types of the argument.
-- @return The contents of the given field.
-- @throws If the field is not one of the allowed types.
local function field(tbl, index, ...)
expect(1, tbl, "table")
expect(2, index, "string")
local value = tbl[index]
local t = native_type(value)
for i = 1, native_select("#", ...) do
if t == native_select(i, ...) then return value end
end
t = get_display_type(value, t)
if value == nil then
error(("field '%s' missing from table"):format(index), 3)
else
error(("bad field '%s' (%s expected, got %s)"):format(index, get_type_names(...), t), 3)
end
end
local function is_nan(num)
return num ~= num
end
--- Expect a number to be within a specific range.
--
-- @tparam number num The value to check.
-- @tparam[opt=-math.huge] number min The minimum value.
-- @tparam[opt=math.huge] number max The maximum value.
-- @return The given `value`.
-- @throws If the value is outside of the allowed range.
-- @since 1.96.0
local function range(num, min, max)
expect(1, num, "number")
min = expect(2, min, "number", "nil") or -math.huge
max = expect(3, max, "number", "nil") or math.huge
if min > max then
error("min must be less than or equal to max)", 2)
end
if is_nan(num) or num < min or num > max then
error(("number outside of range (expected %s to be within %s and %s)"):format(num, min, max), 3)
end
return num
end
return setmetatable({
expect = expect,
field = field,
range = range,
}, { __call = function(_, ...) return expect(...) end })

View File

@@ -0,0 +1,104 @@
-- SPDX-FileCopyrightText: 2017 Daniel Ratcliffe
--
-- SPDX-License-Identifier: LicenseRef-CCPL
--[[- A simple menu bar.
> [!DANGER]
> This is an internal module and SHOULD NOT be used in your own code. It may
> be removed or changed at any time.
This provides a shared implementation of the menu bar used by the `edit` and
`paint` programs. This draws a menu bar at the bottom of the string, with a list
of options.
@local
]]
--[[- Create a new menu bar.
This should be called every time the menu is displayed.
@tparam { string... } items The menu items to display.
@return The menu.
]]
local function create(items)
return {
items = items,
selected = 1,
}
end
--[[- Draw the menu bar at the bottom of the screen.
This should be called when first displaying the menu, and if the whole screen is
redrawn (e.g. after a [`term_resize`]).
@param menu The menu bar to draw.
]]
local function draw(menu)
local _, height = term.getSize()
term.setCursorPos(1, height)
term.clearLine()
local active_color = colors.yellow
term.setTextColor(colors.white)
for k, v in pairs(menu.items) do
if menu.selected == k then
term.setTextColor(active_color)
term.write("[")
term.setTextColor(colors.white)
term.write(v)
term.setTextColor(active_color)
term.write("]")
term.setTextColor(colors.white)
else
term.write(" " .. v .. " ")
end
end
end
--[[- Process an event.
@param menu The menu bar to update.
@tparam string The event name.
@param ... Additional arguments to the event.
@treturn nil|boolean|string Either:
- If no action was taken, return `nil`.
- If the menu was closed, return `false`.
- If an item was selected, return the item as a string.
]]
local function handle_event(menu, key)
if key == keys.right then
-- Move right
menu.selected = menu.selected + 1
if menu.selected > #menu.items then menu.selected = 1 end
draw(menu)
elseif key == keys.left and menu.selected > 1 then
-- Move left
menu.selected = menu.selected - 1
if menu.selected < 1 then menu.selected = #menu.items end
draw(menu)
elseif key == keys.enter or key == keys.numPadEnter then
-- Select an option
return menu.items[menu.selected]
elseif key == keys.control or keys == keys.alt then
-- Cancel the menu
return false
elseif key:lower() >= "a" and key:lower() <= "z" then
-- Select menu items
local char = key:lower()
for _, item in pairs(menu.items) do
if item:sub(1, 1):lower() == char then return item end
end
end
return nil
end
return { create = create, draw = draw, handle_event = handle_event }

View File

@@ -0,0 +1,659 @@
-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- The error messages reported by our lexer and parser.
> [!DANGER]
> This is an internal module and SHOULD NOT be used in your own code. It may
> be removed or changed at any time.
This provides a list of factory methods which take source positions and produce
appropriate error messages targeting that location. These error messages can
then be displayed to the user via [`cc.internal.error_printer`].
@local
]]
local pretty = require "cc.pretty"
local expect = require "cc.expect".expect
local tokens = require "cc.internal.syntax.parser".tokens
local function annotate(start_pos, end_pos, msg)
if msg == nil and (type(end_pos) == "string" or type(end_pos) == "table" or type(end_pos) == "nil") then
end_pos, msg = start_pos, end_pos
end
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, msg, "string", "table", "nil")
return { tag = "annotate", start_pos = start_pos, end_pos = end_pos, msg = msg or "" }
end
--- Format a string as a non-highlighted block of code.
--
-- @tparam string msg The code to format.
-- @treturn cc.pretty.Doc The formatted code.
local function code(msg) return pretty.text(msg, colors.lightGrey) end
--- Maps tokens to a more friendly version.
local token_names = setmetatable({
-- Specific tokens.
[tokens.IDENT] = "identifier",
[tokens.NUMBER] = "number",
[tokens.STRING] = "string",
[tokens.EOF] = "end of file",
-- Symbols and keywords
[tokens.ADD] = code("+"),
[tokens.AND] = code("and"),
[tokens.BREAK] = code("break"),
[tokens.CBRACE] = code("}"),
[tokens.COLON] = code(":"),
[tokens.COMMA] = code(","),
[tokens.CONCAT] = code(".."),
[tokens.CPAREN] = code(")"),
[tokens.CSQUARE] = code("]"),
[tokens.DIV] = code("/"),
[tokens.DO] = code("do"),
[tokens.DOT] = code("."),
[tokens.DOTS] = code("..."),
[tokens.DOUBLE_COLON] = code("::"),
[tokens.ELSE] = code("else"),
[tokens.ELSEIF] = code("elseif"),
[tokens.END] = code("end"),
[tokens.EQ] = code("=="),
[tokens.EQUALS] = code("="),
[tokens.FALSE] = code("false"),
[tokens.FOR] = code("for"),
[tokens.FUNCTION] = code("function"),
[tokens.GE] = code(">="),
[tokens.GOTO] = code("goto"),
[tokens.GT] = code(">"),
[tokens.IF] = code("if"),
[tokens.IN] = code("in"),
[tokens.LE] = code("<="),
[tokens.LEN] = code("#"),
[tokens.LOCAL] = code("local"),
[tokens.LT] = code("<"),
[tokens.MOD] = code("%"),
[tokens.MUL] = code("*"),
[tokens.NE] = code("~="),
[tokens.NIL] = code("nil"),
[tokens.NOT] = code("not"),
[tokens.OBRACE] = code("{"),
[tokens.OPAREN] = code("("),
[tokens.OR] = code("or"),
[tokens.OSQUARE] = code("["),
[tokens.POW] = code("^"),
[tokens.REPEAT] = code("repeat"),
[tokens.RETURN] = code("return"),
[tokens.SEMICOLON] = code(";"),
[tokens.SUB] = code("-"),
[tokens.THEN] = code("then"),
[tokens.TRUE] = code("true"),
[tokens.UNTIL] = code("until"),
[tokens.WHILE] = code("while"),
}, { __index = function(_, name) error("No such token " .. tostring(name), 2) end })
local errors = {}
--------------------------------------------------------------------------------
-- Lexer errors
--------------------------------------------------------------------------------
--[[- A string which ends without a closing quote.
@tparam number start_pos The start position of the string.
@tparam number end_pos The end position of the string.
@tparam string quote The kind of quote (`"` or `'`).
@return The resulting parse error.
]]
function errors.unfinished_string(start_pos, end_pos, quote)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, quote, "string")
return {
"This string is not finished. Are you missing a closing quote (" .. code(quote) .. ")?",
annotate(start_pos, "String started here."),
annotate(end_pos, "Expected a closing quote here."),
}
end
--[[- A string which ends with an escape sequence (so a literal `"foo\`). This
is slightly different from [`unfinished_string`], as we don't want to suggest
adding a quote.
@tparam number start_pos The start position of the string.
@tparam number end_pos The end position of the string.
@tparam string quote The kind of quote (`"` or `'`).
@return The resulting parse error.
]]
function errors.unfinished_string_escape(start_pos, end_pos, quote)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, quote, "string")
return {
"This string is not finished.",
annotate(start_pos, "String started here."),
annotate(end_pos, "An escape sequence was started here, but with nothing following it."),
}
end
--[[- A long string was never finished.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number ;em The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.unfinished_long_string(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"This string was never finished.",
annotate(start_pos, end_pos, "String was started here."),
"We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this string was started.",
}
end
--[[- Malformed opening to a long string (i.e. `[=`).
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number len The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.malformed_long_string(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"Incorrect start of a long string.",
annotate(start_pos, end_pos),
"Tip: If you wanted to start a long string here, add an extra " .. code("[") .. " here.",
}
end
--[[- Malformed nesting of a long string.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@return The resulting parse error.
]]
function errors.nested_long_str(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
code("[[") .. " cannot be nested inside another " .. code("[[ ... ]]"),
annotate(start_pos, end_pos),
}
end
--[[- A malformed numeric literal.
@tparam number start_pos The start position of the number.
@tparam number end_pos The end position of the number.
@return The resulting parse error.
]]
function errors.malformed_number(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"This isn't a valid number.",
annotate(start_pos, end_pos),
"Numbers must be in one of the following formats: " .. code("123") .. ", "
.. code("3.14") .. ", " .. code("23e35") .. ", " .. code("0x01AF") .. ".",
}
end
--[[- A long comment was never finished.
@tparam number start_pos The start position of the long string delimiter.
@tparam number end_pos The end position of the long string delimiter.
@tparam number len The length of the long string delimiter, excluding the first `[`.
@return The resulting parse error.
]]
function errors.unfinished_long_comment(start_pos, end_pos, len)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
expect(3, len, "number")
return {
"This comment was never finished.",
annotate(start_pos, end_pos, "Comment was started here."),
"We expected a closing delimiter (" .. code("]" .. ("="):rep(len - 1) .. "]") .. ") somewhere after this comment was started.",
}
end
--[[- `&&` was used instead of `and`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_and(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("and") .. " to check if both values are true.",
}
end
--[[- `||` was used instead of `or`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_or(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("or") .. " to check if either value is true.",
}
end
--[[- `!=` was used instead of `~=`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_ne(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("~=") .. " to check if two values are not equal.",
}
end
--[[- `!` was used instead of `not`.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.wrong_not(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected character.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("not") .. " to negate a boolean.",
}
end
--[[- An unexpected character was used.
@tparam number pos The position of this character.
@return The resulting parse error.
]]
function errors.unexpected_character(pos)
expect(1, pos, "number")
return {
"Unexpected character.",
annotate(pos, "This character isn't usable in Lua code."),
}
end
--------------------------------------------------------------------------------
-- Expression parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we expected an expression but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_expression(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected an expression.",
annotate(start_pos, end_pos),
}
end
--[[- A fallback error when we expected a variable but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_var(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected a variable name.",
annotate(start_pos, end_pos),
}
end
--[[- `=` was used in an expression context.
@tparam number start_pos The start position of the `=` token.
@tparam number end_pos The end position of the `=` token.
@return The resulting parse error.
]]
function errors.use_double_equals(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected " .. code("=") .. " in expression.",
annotate(start_pos, end_pos),
"Tip: Replace this with " .. code("==") .. " to check if two values are equal.",
}
end
--[[- `=` was used after an expression inside a table.
@tparam number start_pos The start position of the `=` token.
@tparam number end_pos The end position of the `=` token.
@return The resulting parse error.
]]
function errors.table_key_equals(start_pos, end_pos)
expect(1, start_pos, "number")
expect(2, end_pos, "number")
return {
"Unexpected " .. code("=") .. " in expression.",
annotate(start_pos, end_pos),
"Tip: Wrap the preceding expression in " .. code("[") .. " and " .. code("]") .. " to use it as a table key.",
}
end
--[[- There is a trailing comma in this list of function arguments.
@tparam number token The token id.
@tparam number token_start The start position of the token.
@tparam number token_end The end position of the token.
@tparam number prev The start position of the previous entry.
@treturn table The resulting parse error.
]]
function errors.missing_table_comma(token, token_start, token_end, prev)
expect(1, token, "number")
expect(2, token_start, "number")
expect(3, token_end, "number")
expect(4, prev, "number")
return {
"Unexpected " .. token_names[token] .. " in table.",
annotate(token_start, token_end),
annotate(prev + 1, prev + 1, "Are you missing a comma here?"),
}
end
--[[- There is a trailing comma in this list of function arguments.
@tparam number comma_start The start position of the `,` token.
@tparam number comma_end The end position of the `,` token.
@tparam number paren_start The start position of the `)` token.
@tparam number paren_end The end position of the `)` token.
@treturn table The resulting parse error.
]]
function errors.trailing_call_comma(comma_start, comma_end, paren_start, paren_end)
expect(1, comma_start, "number")
expect(2, comma_end, "number")
expect(3, paren_start, "number")
expect(4, paren_end, "number")
return {
"Unexpected " .. code(")") .. " in function call.",
annotate(paren_start, paren_end),
annotate(comma_start, comma_end, "Tip: Try removing this " .. code(",") .. "."),
}
end
--------------------------------------------------------------------------------
-- Statement parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we expected a statement but received another token.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_statement(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ". Expected a statement.",
annotate(start_pos, end_pos),
}
end
--[[- `local function` was used with a table identifier.
@tparam number local_start The start position of the `local` token.
@tparam number local_end The end position of the `local` token.
@tparam number dot_start The start position of the `.` token.
@tparam number dot_end The end position of the `.` token.
@return The resulting parse error.
]]
function errors.local_function_dot(local_start, local_end, dot_start, dot_end)
expect(1, local_start, "number")
expect(2, local_end, "number")
expect(3, dot_start, "number")
expect(4, dot_end, "number")
return {
"Cannot use " .. code("local function") .. " with a table key.",
annotate(dot_start, dot_end, code(".") .. " appears here."),
annotate(local_start, local_end, "Tip: " .. "Try removing this " .. code("local") .. " keyword."),
}
end
--[[- A statement of the form `x.y`
@tparam number token The token id.
@tparam number pos The position right after this name.
@return The resulting parse error.
]]
function errors.standalone_name(token, pos)
expect(1, token, "number")
expect(2, pos, "number")
return {
"Unexpected " .. token_names[token] .. " after name.",
annotate(pos),
"Did you mean to assign this or call it as a function?",
}
end
--[[- A statement of the form `x.y, z`
@tparam number token The token id.
@tparam number pos The position right after this name.
@return The resulting parse error.
]]
function errors.standalone_names(token, pos)
expect(1, token, "number")
expect(2, pos, "number")
return {
"Unexpected " .. token_names[token] .. " after name.",
annotate(pos),
"Did you mean to assign this?",
}
end
--[[- A statement of the form `x.y`. This is similar to [`standalone_name`], but
when the next token is on another line.
@tparam number token The token id.
@tparam number pos The position right after this name.
@return The resulting parse error.
]]
function errors.standalone_name_call(token, pos)
expect(1, token, "number")
expect(2, pos, "number")
return {
"Unexpected " .. token_names[token] .. " after name.",
annotate(pos + 1, "Expected something before the end of the line."),
"Tip: Use " .. code("()") .. " to call with no arguments.",
}
end
--[[- `then` was expected
@tparam number if_start The start position of the `if`/`elseif` keyword.
@tparam number if_end The end position of the `if`/`elseif` keyword.
@tparam number token_pos The current token position.
@return The resulting parse error.
]]
function errors.expected_then(if_start, if_end, token_pos)
expect(1, if_start, "number")
expect(2, if_end, "number")
expect(3, token_pos, "number")
return {
"Expected " .. code("then") .. " after if condition.",
annotate(if_start, if_end, "If statement started here."),
annotate(token_pos, "Expected " .. code("then") .. " before here."),
}
end
--[[- `end` was expected
@tparam number block_start The start position of the block.
@tparam number block_end The end position of the block.
@tparam number token The current token position.
@tparam number token_start The current token position.
@tparam number token_end The current token position.
@return The resulting parse error.
]]
function errors.expected_end(block_start, block_end, token, token_start, token_end)
return {
"Unexpected " .. token_names[token] .. ". Expected " .. code("end") .. " or another statement.",
annotate(block_start, block_end, "Block started here."),
annotate(token_start, token_end, "Expected end of block here."),
}
end
--[[- An unexpected `end` in a statement.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.unexpected_end(start_pos, end_pos)
return {
"Unexpected " .. code("end") .. ".",
annotate(start_pos, end_pos),
"Your program contains more " .. code("end") .. "s than needed. Check " ..
"each block (" .. code("if") .. ", " .. code("for") .. ", " ..
code("function") .. ", ...) only has one " .. code("end") .. ".",
}
end
--[[- A label statement was opened but not closed.
@tparam number open_start The start position of the opening label.
@tparam number open_end The end position of the opening label.
@tparam number tok_start The start position of the current token.
@return The resulting parse error.
]]
function errors.unclosed_label(open_start, open_end, token, start_pos, end_pos)
expect(1, open_start, "number")
expect(2, open_end, "number")
expect(3, token, "number")
expect(4, start_pos, "number")
expect(5, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ".",
annotate(open_start, open_end, "Label was started here."),
annotate(start_pos, end_pos, "Tip: Try adding " .. code("::") .. " here."),
}
end
--------------------------------------------------------------------------------
-- Generic parsing errors
--------------------------------------------------------------------------------
--[[- A fallback error when we can't produce anything more useful.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.unexpected_token(token, start_pos, end_pos)
expect(1, token, "number")
expect(2, start_pos, "number")
expect(3, end_pos, "number")
return {
"Unexpected " .. token_names[token] .. ".",
annotate(start_pos, end_pos),
}
end
--[[- A parenthesised expression was started but not closed.
@tparam number open_start The start position of the opening bracket.
@tparam number open_end The end position of the opening bracket.
@tparam number tok_start The start position of the opening bracket.
@return The resulting parse error.
]]
function errors.unclosed_brackets(open_start, open_end, token, start_pos, end_pos)
expect(1, open_start, "number")
expect(2, open_end, "number")
expect(3, token, "number")
expect(4, start_pos, "number")
expect(5, end_pos, "number")
-- TODO: Do we want to be smarter here with where we report the error?
return {
"Unexpected " .. token_names[token] .. ". Are you missing a closing bracket?",
annotate(open_start, open_end, "Brackets were opened here."),
annotate(start_pos, end_pos, "Unexpected " .. token_names[token] .. " here."),
}
end
--[[- Expected `(` to open our function arguments.
@tparam number token The token id.
@tparam number start_pos The start position of the token.
@tparam number end_pos The end position of the token.
@return The resulting parse error.
]]
function errors.expected_function_args(token, start_pos, end_pos)
return {
"Unexpected " .. token_names[token] .. ". Expected " .. code("(") .. " to start function arguments.",
annotate(start_pos, end_pos),
}
end
return errors

View File

@@ -0,0 +1,175 @@
-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- The main entrypoint to our Lua parser
> [!DANGER]
> This is an internal module and SHOULD NOT be used in your own code. It may
> be removed or changed at any time.
@local
]]
local expect = require "cc.expect".expect
local lex_one = require "cc.internal.syntax.lexer".lex_one
local parser = require "cc.internal.syntax.parser"
local error_printer = require "cc.internal.error_printer"
local error_sentinel = {}
local function make_context(input)
expect(1, input, "string")
local context = {}
local lines = { 1 }
function context.line(pos) lines[#lines + 1] = pos end
function context.get_pos(pos)
expect(1, pos, "number")
for i = #lines, 1, -1 do
local start = lines[i]
if pos >= start then return i, pos - start + 1 end
end
error("Position is <= 0", 2)
end
function context.get_line(pos)
expect(1, pos, "number")
for i = #lines, 1, -1 do
local start = lines[i]
if pos >= start then return input:match("[^\r\n]*", start) end
end
error("Position is <= 0", 2)
end
return context
end
local function make_lexer(input, context)
local tokens, last_token = parser.tokens, parser.tokens.COMMENT
local pos = 1
return function()
while true do
local token, start, finish = lex_one(context, input, pos)
if not token then return tokens.EOF, #input + 1, #input + 1 end
pos = finish + 1
if token < last_token then
return token, start, finish
elseif token == tokens.ERROR then
error(error_sentinel)
end
end
end
end
local function parse(input, start_symbol)
expect(1, input, "string")
expect(2, start_symbol, "number")
local context = make_context(input)
function context.report(msg, ...)
expect(1, msg, "table", "function")
if type(msg) == "function" then msg = msg(...) end
error_printer(context, msg)
error(error_sentinel)
end
local ok, err = pcall(parser.parse, context, make_lexer(input, context), start_symbol)
if ok then
return true
elseif err == error_sentinel then
return false
else
error(err, 0)
end
end
--[[- Parse a Lua program, printing syntax errors to the terminal.
@tparam string input The string to parse.
@treturn boolean Whether the string was successfully parsed.
]]
local function parse_program(input) return parse(input, parser.program) end
--[[- Parse a REPL input (either a program or a list of expressions), printing
syntax errors to the terminal.
@tparam string input The string to parse.
@treturn boolean Whether the string was successfully parsed.
]]
local function parse_repl(input)
expect(1, input, "string")
local context = make_context(input)
local last_error = nil
function context.report(msg, ...)
expect(1, msg, "table", "function")
if type(msg) == "function" then msg = msg(...) end
last_error = msg
error(error_sentinel)
end
local lexer = make_lexer(input, context)
local parsers = {}
for i, start_code in ipairs { parser.repl_exprs, parser.program } do
parsers[i] = coroutine.create(parser.parse)
assert(coroutine.resume(parsers[i], context, coroutine.yield, start_code))
end
-- Run all parsers together in parallel, feeding them one token at a time.
-- Once all parsers have failed, report the last failure (corresponding to
-- the longest parse).
local ok, err = pcall(function()
local parsers_n = #parsers
while true do
local token, start, finish = lexer()
local all_failed = true
for i = 1, parsers_n do
local parser = parsers[i]
if parser then
local ok, err = coroutine.resume(parser, token, start, finish)
if ok then
-- This parser accepted our input, succeed immediately.
if coroutine.status(parser) == "dead" then return end
all_failed = false -- Otherwise continue parsing.
elseif err ~= error_sentinel then
-- An internal error occurred: propagate it.
error(err, 0)
else
-- The parser failed, stub it out so we don't try to continue using it.
parsers[i] = false
end
end
end
if all_failed then error(error_sentinel) end
end
end)
if ok then
return true
elseif err == error_sentinel then
error_printer(context, last_error)
return false
else
error(err, 0)
end
end
return {
parse_program = parse_program,
parse_repl = parse_repl,
}

View File

@@ -0,0 +1,422 @@
-- SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- A lexer for Lua source code.
> [!DANGER]
> This is an internal module and SHOULD NOT be used in your own code. It may
> be removed or changed at any time.
This module provides utilities for lexing Lua code, returning tokens compatible
with [`cc.internal.syntax.parser`]. While all lexers are roughly the same, there
are some design choices worth drawing attention to:
- The lexer uses Lua patterns (i.e. [`string.find`]) as much as possible,
trying to avoid [`string.sub`] loops except when needed. This allows us to
move string processing to native code, which ends up being much faster.
- We try to avoid allocating where possible. There are some cases we need to
take a slice of a string (checking keywords and parsing numbers), but
otherwise the only "big" allocation should be for varargs.
- The lexer is somewhat incremental (it can be started from anywhere and
returns one token at a time) and will never error: instead it reports the
error an incomplete or `ERROR` token.
@local
]]
local errors = require "cc.internal.syntax.errors"
local tokens = require "cc.internal.syntax.parser".tokens
local sub, find = string.sub, string.find
local keywords = {
["and"] = tokens.AND, ["break"] = tokens.BREAK, ["do"] = tokens.DO, ["else"] = tokens.ELSE,
["elseif"] = tokens.ELSEIF, ["end"] = tokens.END, ["false"] = tokens.FALSE, ["for"] = tokens.FOR,
["function"] = tokens.FUNCTION, ["goto"] = tokens.GOTO, ["if"] = tokens.IF, ["in"] = tokens.IN,
["local"] = tokens.LOCAL, ["nil"] = tokens.NIL, ["not"] = tokens.NOT, ["or"] = tokens.OR,
["repeat"] = tokens.REPEAT, ["return"] = tokens.RETURN, ["then"] = tokens.THEN, ["true"] = tokens.TRUE,
["until"] = tokens.UNTIL, ["while"] = tokens.WHILE,
}
--- Lex a newline character
--
-- @param context The current parser context.
-- @tparam string str The current string.
-- @tparam number pos The position of the newline character.
-- @tparam string nl The current new line character, either "\n" or "\r".
-- @treturn pos The new position, after the newline.
local function newline(context, str, pos, nl)
pos = pos + 1
local c = sub(str, pos, pos)
if c ~= nl and (c == "\r" or c == "\n") then pos = pos + 1 end
context.line(pos) -- Mark the start of the next line.
return pos
end
--- Lex a number
--
-- @param context The current parser context.
-- @tparam string str The current string.
-- @tparam number start The start position of this number.
-- @treturn number The token id for numbers.
-- @treturn number The end position of this number
local function lex_number(context, str, start)
local pos = start + 1
local exp_low, exp_high = "e", "E"
if sub(str, start, start) == "0" then
local next = sub(str, pos, pos)
if next == "x" or next == "X" then
pos = pos + 1
exp_low, exp_high = "p", "P"
end
end
while true do
local c = sub(str, pos, pos)
if c == exp_low or c == exp_high then
pos = pos + 1
c = sub(str, pos, pos)
if c == "+" or c == "-" then
pos = pos + 1
end
elseif (c >= "0" and c <= "9") or (c >= "a" and c <= "f") or (c >= "A" and c <= "F") or c == "." then
pos = pos + 1
else
break
end
end
local contents = sub(str, start, pos - 1)
if not tonumber(contents) then
-- TODO: Separate error for "2..3"?
context.report(errors.malformed_number, start, pos - 1)
end
return tokens.NUMBER, pos - 1
end
local lex_string_zap
--[[- Lex a quoted string.
@param context The current parser context.
@tparam string str The string we're lexing.
@tparam number pos The position to start lexing from.
@tparam number start_pos The actual start position of the string.
@tparam string quote The quote character, either " or '.
@treturn number The token id for strings.
@treturn number The new position.
@treturn nil A placeholder value.
@treturn table|nil The continuation function when the string is not finished.
]]
local function lex_string(context, str, pos, start_pos, quote)
while true do
local c = sub(str, pos, pos)
if c == quote then
return tokens.STRING, pos
elseif c == "\n" or c == "\r" or c == "" then
-- We don't call newline here, as that's done for the next token.
context.report(errors.unfinished_string, start_pos, pos, quote)
return tokens.STRING, pos - 1
elseif c == "\\" then
c = sub(str, pos + 1, pos + 1)
if c == "\n" or c == "\r" then
pos = newline(context, str, pos + 1, c)
elseif c == "" then
context.report(errors.unfinished_string_escape, start_pos, pos, quote)
return tokens.STRING, pos, nil, { lex_string, 1, 1, quote }
elseif c == "z" then
return lex_string_zap(context, str, pos + 2, start_pos, quote)
else
pos = pos + 2
end
else
pos = pos + 1
end
end
end
--[[- Lex the remainder of a zap escape sequence (`\z`). This consumes all leading
whitespace, and then continues lexing the string.
@param context The current parser context.
@tparam string str The string we're lexing.
@tparam number pos The position to start lexing from.
@tparam number start_pos The actual start position of the string.
@tparam string quote The quote character, either " or '.
@treturn number The token id for strings.
@treturn number The new position.
@treturn nil A placeholder value.
@treturn table|nil The continuation function when the string is not finished.
]]
lex_string_zap = function(context, str, pos, start_pos, quote)
while true do
local next_pos, _, c = find(str, "([%S\r\n])", pos)
if not next_pos then
context.report(errors.unfinished_string, start_pos, #str, quote)
return tokens.STRING, #str, nil, { lex_string_zap, 1, 1, quote }
end
if c == "\n" or c == "\r" then
pos = newline(context, str, next_pos, c)
else
pos = next_pos
break
end
end
return lex_string(context, str, pos, start_pos, quote)
end
--- Consume the start or end of a long string.
-- @tparam string str The input string.
-- @tparam number pos The start position. This must be after the first `[` or `]`.
-- @tparam string fin The terminating character, either `[` or `]`.
-- @treturn boolean Whether a long string was successfully started.
-- @treturn number The current position.
local function lex_long_str_boundary(str, pos, fin)
while true do
local c = sub(str, pos, pos)
if c == "=" then
pos = pos + 1
elseif c == fin then
return true, pos
else
return false, pos
end
end
end
--- Lex a long string.
-- @param context The current parser context.
-- @tparam string str The input string.
-- @tparam number start The start position, after the input boundary.
-- @tparam number len The expected length of the boundary. Equal to 1 + the
-- number of `=`.
-- @treturn number|nil The end position, or [`nil`] if this is not terminated.
local function lex_long_str(context, str, start, len)
local pos = start
while true do
pos = find(str, "[%[%]\n\r]", pos)
if not pos then return nil end
local c = sub(str, pos, pos)
if c == "]" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "]")
if ok and boundary_pos - pos == len then
return boundary_pos
else
pos = boundary_pos
end
elseif c == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
if ok and boundary_pos - pos == len and len == 1 then
context.report(errors.nested_long_str, pos, boundary_pos)
end
pos = boundary_pos
else
pos = newline(context, str, pos, c)
end
end
end
--[[- Lex the remainder of a long string.
@param context The current parser context.
@tparam string str The string we're lexing.
@tparam number pos The position to start lexing from.
@tparam number start_pos The actual start position of the string.
@tparam number boundary_length The length of the boundary.
@treturn number The token id for strings.
@treturn number The new position.
@treturn nil A placeholder value.
@treturn table|nil The continuation function when the string is not finished.
]]
local function lex_long_string(context, str, pos, start_pos, boundary_length)
local end_pos = lex_long_str(context, str, pos, boundary_length)
if end_pos then return tokens.STRING, end_pos end
context.report(errors.unfinished_long_string, start_pos, pos - 1, boundary_length)
return tokens.STRING, #str, nil, { lex_long_string, 0, 0, boundary_length }
end
--[[- Lex the remainder of a long comment.
@param context The current parser context.
@tparam string str The comment we're lexing.
@tparam number pos The position to start lexing from.
@tparam number start_pos The actual start position of the comment.
@tparam number boundary_length The length of the boundary.
@treturn number The token id for comments.
@treturn number The new position.
@treturn nil A placeholder value.
@treturn table|nil The continuation function when the comment is not finished.
]]
local function lex_long_comment(context, str, pos, start_pos, boundary_length)
local end_pos = lex_long_str(context, str, pos, boundary_length)
if end_pos then return tokens.COMMENT, end_pos end
context.report(errors.unfinished_long_comment, start_pos, pos - 1, boundary_length)
return tokens.COMMENT, #str, nil, { lex_long_comment, 0, 0, boundary_length }
end
--- Lex a single token, assuming we have removed all leading whitespace.
--
-- @param context The current parser context.
-- @tparam string str The string we're lexing.
-- @tparam number pos The start position.
-- @treturn number The id of the parsed token.
-- @treturn number The end position of this token.
-- @treturn string|nil The token's current contents (only given for identifiers)
local function lex_token(context, str, pos)
local c = sub(str, pos, pos)
-- Identifiers and keywords
if (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_" then
local _, end_pos = find(str, "^[%w_]+", pos)
if not end_pos then error("Impossible: No position") end
local contents = sub(str, pos, end_pos)
return keywords[contents] or tokens.IDENT, end_pos, contents
-- Numbers
elseif c >= "0" and c <= "9" then return lex_number(context, str, pos)
-- Strings
elseif c == "\"" or c == "\'" then return lex_string(context, str, pos + 1, pos, c)
elseif c == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, pos + 1, "[")
if ok then -- Long string
return lex_long_string(context, str, boundary_pos + 1, pos, boundary_pos - pos)
elseif pos + 1 == boundary_pos then -- Just a "["
return tokens.OSQUARE, pos
else -- Malformed long string, for instance "[="
context.report(errors.malformed_long_string, pos, boundary_pos, boundary_pos - pos)
return tokens.ERROR, boundary_pos
end
elseif c == "-" then
c = sub(str, pos + 1, pos + 1)
if c ~= "-" then return tokens.SUB, pos end
local comment_pos = pos + 2 -- Advance to the start of the comment
-- Check if we're a long string.
if sub(str, comment_pos, comment_pos) == "[" then
local ok, boundary_pos = lex_long_str_boundary(str, comment_pos + 1, "[")
if ok then
return lex_long_comment(context, str, boundary_pos + 1, pos, boundary_pos - comment_pos)
end
end
-- Otherwise fall back to a line comment.
local _, end_pos = find(str, "^[^\n\r]*", comment_pos)
return tokens.COMMENT, end_pos
elseif c == "." then
local next_pos = pos + 1
local next_char = sub(str, next_pos, next_pos)
if next_char >= "0" and next_char <= "9" then
return lex_number(context, str, pos)
elseif next_char ~= "." then
return tokens.DOT, pos
end
if sub(str, pos + 2, pos + 2) ~= "." then return tokens.CONCAT, next_pos end
return tokens.DOTS, pos + 2
elseif c == "=" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.EQ, next_pos end
return tokens.EQUALS, pos
elseif c == ">" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
return tokens.GT, pos
elseif c == "<" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == "=" then return tokens.LE, next_pos end
return tokens.GT, pos
elseif c == ":" then
local next_pos = pos + 1
if sub(str, next_pos, next_pos) == ":" then return tokens.DOUBLE_COLON, next_pos end
return tokens.COLON, pos
elseif c == "~" and sub(str, pos + 1, pos + 1) == "=" then return tokens.NE, pos + 1
-- Single character tokens
elseif c == "," then return tokens.COMMA, pos
elseif c == ";" then return tokens.SEMICOLON, pos
elseif c == "(" then return tokens.OPAREN, pos
elseif c == ")" then return tokens.CPAREN, pos
elseif c == "]" then return tokens.CSQUARE, pos
elseif c == "{" then return tokens.OBRACE, pos
elseif c == "}" then return tokens.CBRACE, pos
elseif c == "*" then return tokens.MUL, pos
elseif c == "/" then return tokens.DIV, pos
elseif c == "#" then return tokens.LEN, pos
elseif c == "%" then return tokens.MOD, pos
elseif c == "^" then return tokens.POW, pos
elseif c == "+" then return tokens.ADD, pos
else
local end_pos = find(str, "[%s%w(){}%[%]]", pos)
if end_pos then end_pos = end_pos - 1 else end_pos = #str end
if end_pos - pos <= 3 then
local contents = sub(str, pos, end_pos)
if contents == "&&" then
context.report(errors.wrong_and, pos, end_pos)
return tokens.AND, end_pos
elseif contents == "||" then
context.report(errors.wrong_or, pos, end_pos)
return tokens.OR, end_pos
elseif contents == "!=" or contents == "<>" then
context.report(errors.wrong_ne, pos, end_pos)
return tokens.NE, end_pos
elseif contents == "!" then
context.report(errors.wrong_not, pos, end_pos)
return tokens.NOT, end_pos
end
end
context.report(errors.unexpected_character, pos)
return tokens.ERROR, end_pos
end
end
--[[- Lex a single token from an input string.
@param context The current parser context.
@tparam string str The string we're lexing.
@tparam number pos The start position.
@treturn[1] number The id of the parsed token.
@treturn[1] number The start position of this token.
@treturn[1] number The end position of this token.
@treturn[1] string|nil The token's current contents (only given for identifiers)
@treturn[2] nil If there are no more tokens to consume
]]
local function lex_one(context, str, pos)
while true do
local start_pos, _, c = find(str, "([%S\r\n])", pos)
if not start_pos then
return
elseif c == "\r" or c == "\n" then
pos = newline(context, str, start_pos, c)
else
local token_id, end_pos, content, continue = lex_token(context, str, start_pos)
return token_id, start_pos, end_pos, content, continue
end
end
end
return {
lex_one = lex_one,
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,526 @@
-- SPDX-FileCopyrightText: 2020 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0
--[[- A pretty printer for rendering data structures in an aesthetically
pleasing manner.
In order to display something using [`cc.pretty`], you build up a series of
[documents][`Doc`]. These behave a little bit like strings; you can concatenate
them together and then print them to the screen.
However, documents also allow you to control how they should be printed. There
are several functions (such as [`nest`] and [`group`]) which allow you to control
the "layout" of the document. When you come to display the document, the 'best'
(most compact) layout is used.
The structure of this module is based on [A Prettier Printer][prettier].
[prettier]: https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf "A Prettier Printer"
@module cc.pretty
@since 1.87.0
@usage Print a table to the terminal
local pretty = require "cc.pretty"
pretty.pretty_print({ 1, 2, 3 })
@usage Build a custom document and display it
local pretty = require "cc.pretty"
pretty.print(pretty.group(pretty.text("hello") .. pretty.space_line .. pretty.text("world")))
]]
local expect = require "cc.expect"
local expect, field = expect.expect, expect.field
local type, getmetatable, setmetatable, colors, str_write, tostring = type, getmetatable, setmetatable, colors, write, tostring
local debug_info, debug_local = debug.getinfo, debug.getlocal
--- [`table.insert`] alternative, but with the length stored inline.
local function append(out, value)
local n = out.n + 1
out[n], out.n = value, n
end
--- A document containing formatted text, with multiple possible layouts.
--
-- Documents effectively represent a sequence of strings in alternative layouts,
-- which we will try to print in the most compact form necessary.
--
-- @type Doc
local Doc = { }
local function mk_doc(tbl) return setmetatable(tbl, Doc) end
--- An empty document.
local empty = mk_doc({ tag = "nil" })
--- A document with a single space in it.
local space = mk_doc({ tag = "text", text = " " })
--- A line break. When collapsed with [`group`], this will be replaced with [`empty`].
local line = mk_doc({ tag = "line", flat = empty })
--- A line break. When collapsed with [`group`], this will be replaced with [`space`].
local space_line = mk_doc({ tag = "line", flat = space })
local text_cache = { [""] = empty, [" "] = space, ["\n"] = space_line }
local function mk_text(text, color)
return text_cache[text] or setmetatable({ tag = "text", text = text, color = color }, Doc)
end
--- Create a new document from a string.
--
-- If your string contains multiple lines, [`group`] will flatten the string
-- into a single line, with spaces between each line.
--
-- @tparam string text The string to construct a new document with.
-- @tparam[opt] number color The color this text should be printed with. If not given, we default to the current
-- color.
-- @treturn Doc The document with the provided text.
-- @usage Write some blue text.
--
-- local pretty = require "cc.pretty"
-- pretty.print(pretty.text("Hello!", colors.blue))
local function text(text, color)
expect(1, text, "string")
expect(2, color, "number", "nil")
local cached = text_cache[text]
if cached then return cached end
local new_line = text:find("\n", 1)
if not new_line then return mk_text(text, color) end
-- Split the string by "\n". With a micro-optimisation to skip empty strings.
local doc = setmetatable({ tag = "concat", n = 0 }, Doc)
if new_line ~= 1 then append(doc, mk_text(text:sub(1, new_line - 1), color)) end
new_line = new_line + 1
while true do
local next_line = text:find("\n", new_line)
append(doc, space_line)
if not next_line then
if new_line <= #text then append(doc, mk_text(text:sub(new_line), color)) end
return doc
else
if new_line <= next_line - 1 then
append(doc, mk_text(text:sub(new_line, next_line - 1), color))
end
new_line = next_line + 1
end
end
end
--- Concatenate several documents together. This behaves very similar to string concatenation.
--
-- @tparam Doc|string ... The documents to concatenate.
-- @treturn Doc The concatenated documents.
-- @usage
-- local pretty = require "cc.pretty"
-- local doc1, doc2 = pretty.text("doc1"), pretty.text("doc2")
-- print(pretty.concat(doc1, " - ", doc2))
-- print(doc1 .. " - " .. doc2) -- Also supports ..
local function concat(...)
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == "string" then args[i] = text(args[i]) end
if getmetatable(args[i]) ~= Doc then expect(i, args[i], "document") end
end
if args.n == 0 then return empty end
if args.n == 1 then return args[1] end
args.tag = "concat"
return setmetatable(args, Doc)
end
Doc.__concat = concat --- @local
--- Indent later lines of the given document with the given number of spaces.
--
-- For instance, nesting the document
-- ```txt
-- foo
-- bar
-- ```
-- by two spaces will produce
-- ```txt
-- foo
-- bar
-- ```
--
-- @tparam number depth The number of spaces with which the document should be indented.
-- @tparam Doc doc The document to indent.
-- @treturn Doc The nested document.
-- @usage
-- local pretty = require "cc.pretty"
-- print(pretty.nest(2, pretty.text("foo\nbar")))
local function nest(depth, doc)
expect(1, depth, "number")
if getmetatable(doc) ~= Doc then expect(2, doc, "document") end
if depth <= 0 then error("depth must be a positive number", 2) end
return setmetatable({ tag = "nest", depth = depth, doc }, Doc)
end
local function flatten(doc)
if doc.flat then return doc.flat end
local kind = doc.tag
if kind == "nil" or kind == "text" then
return doc
elseif kind == "concat" then
local out = setmetatable({ tag = "concat", n = doc.n }, Doc)
for i = 1, doc.n do out[i] = flatten(doc[i]) end
doc.flat, out.flat = out, out -- cache the flattened node
return out
elseif kind == "nest" then
return flatten(doc[1])
elseif kind == "group" then
return doc[1]
else
error("Unknown doc " .. kind)
end
end
--- Builds a document which is displayed on a single line if there is enough
-- room, or as normal if not.
--
-- @tparam Doc doc The document to group.
-- @treturn Doc The grouped document.
-- @usage Uses group to show things being displayed on one or multiple lines.
--
-- local pretty = require "cc.pretty"
-- local doc = pretty.group("Hello" .. pretty.space_line .. "World")
-- print(pretty.render(doc, 5)) -- On multiple lines
-- print(pretty.render(doc, 20)) -- Collapsed onto one.
local function group(doc)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
if doc.tag == "group" then return doc end -- Skip if already grouped.
local flattened = flatten(doc)
if flattened == doc then return doc end -- Also skip if flattening does nothing.
return setmetatable({ tag = "group", flattened, doc }, Doc)
end
local function get_remaining(doc, width)
local kind = doc.tag
if kind == "nil" or kind == "line" then
return width
elseif kind == "text" then
return width - #doc.text
elseif kind == "concat" then
for i = 1, doc.n do
width = get_remaining(doc[i], width)
if width < 0 then break end
end
return width
elseif kind == "group" or kind == "nest" then
return get_remaining(kind[1])
else
error("Unknown doc " .. kind)
end
end
--- Display a document on the terminal.
--
-- @tparam Doc doc The document to render
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function write(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
local term = term
local width, height = term.getSize()
local ribbon_width = (ribbon_frac or 0.6) * width
if ribbon_width < 0 then ribbon_width = 0 end
if ribbon_width > width then ribbon_width = width end
local def_color = term.getTextColour()
local current_color = def_color
local function go(doc, indent, col)
local kind = doc.tag
if kind == "nil" then
return col
elseif kind == "text" then
local doc_color = doc.color or def_color
if doc_color ~= current_color then
term.setTextColour(doc_color)
current_color = doc_color
end
str_write(doc.text)
return col + #doc.text
elseif kind == "line" then
local _, y = term.getCursorPos()
if y < height then
term.setCursorPos(indent + 1, y + 1)
else
term.scroll(1)
term.setCursorPos(indent + 1, height)
end
return indent
elseif kind == "concat" then
for i = 1, doc.n do col = go(doc[i], indent, col) end
return col
elseif kind == "nest" then
return go(doc[1], indent + doc.depth, col)
elseif kind == "group" then
if get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
return go(doc[1], indent, col)
else
return go(doc[2], indent, col)
end
else
error("Unknown doc " .. kind)
end
end
local col = math.max(term.getCursorPos() - 1, 0)
go(doc, 0, col)
if current_color ~= def_color then term.setTextColour(def_color) end
end
--- Display a document on the terminal with a trailing new line.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
local function print(doc, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, ribbon_frac, "number", "nil")
write(doc, ribbon_frac)
str_write("\n")
end
--- Render a document, converting it into a string.
--
-- @tparam Doc doc The document to render.
-- @tparam[opt] number width The maximum width of this document. Note that long strings will not be wrapped to fit
-- this width - it is only used for finding the best layout.
-- @tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
-- @treturn string The rendered document as a string.
local function render(doc, width, ribbon_frac)
if getmetatable(doc) ~= Doc then expect(1, doc, "document") end
expect(2, width, "number", "nil")
expect(3, ribbon_frac, "number", "nil")
local ribbon_width
if width then
ribbon_width = (ribbon_frac or 0.6) * width
if ribbon_width < 0 then ribbon_width = 0 end
if ribbon_width > width then ribbon_width = width end
end
local out = { n = 0 }
local function go(doc, indent, col)
local kind = doc.tag
if kind == "nil" then
return col
elseif kind == "text" then
append(out, doc.text)
return col + #doc.text
elseif kind == "line" then
append(out, "\n" .. (" "):rep(indent))
return indent
elseif kind == "concat" then
for i = 1, doc.n do col = go(doc[i], indent, col) end
return col
elseif kind == "nest" then
return go(doc[1], indent + doc.depth, col)
elseif kind == "group" then
if not width or get_remaining(doc[1], math.min(width, ribbon_width + indent) - col) >= 0 then
return go(doc[1], indent, col)
else
return go(doc[2], indent, col)
end
else
error("Unknown doc " .. kind)
end
end
go(doc, 0, 0)
return table.concat(out, "", 1, out.n)
end
Doc.__tostring = render --- @local
local keywords = {
["and"] = true, ["break"] = true, ["do"] = true, ["else"] = true,
["elseif"] = true, ["end"] = true, ["false"] = true, ["for"] = true,
["function"] = true, ["if"] = true, ["in"] = true, ["local"] = true,
["nil"] = true, ["not"] = true, ["or"] = true, ["repeat"] = true, ["return"] = true,
["then"] = true, ["true"] = true, ["until"] = true, ["while"] = true,
}
local comma = text(",")
local braces = text("{}")
local obrace, cbrace = text("{"), text("}")
local obracket, cbracket = text("["), text("] = ")
local function key_compare(a, b)
local ta, tb = type(a), type(b)
if ta == "string" then return tb ~= "string" or a < b
elseif tb == "string" then return false
end
if ta == "number" then return tb ~= "number" or a < b end
return false
end
local function show_function(fn, options)
local info = debug_info and debug_info(fn, "Su")
-- Include function source position if available
local name
if options.function_source and info and info.short_src and info.linedefined and info.linedefined >= 1 then
name = "function<" .. info.short_src .. ":" .. info.linedefined .. ">"
else
name = tostring(fn)
end
-- Include arguments if a Lua function and if available. Lua will report "C"
-- functions as variadic.
if options.function_args and info and info.what == "Lua" and info.nparams and debug_local then
local args = {}
for i = 1, info.nparams do args[i] = debug_local(fn, i) or "?" end
if info.isvararg then args[#args + 1] = "..." end
name = name .. "(" .. table.concat(args, ", ") .. ")"
end
return name
end
local function pretty_impl(obj, options, tracking)
local obj_type = type(obj)
if obj_type == "string" then
local formatted = ("%q"):format(obj):gsub("\\\n", "\\n")
return text(formatted, colors.red)
elseif obj_type == "number" then
return text(tostring(obj), colors.magenta)
elseif obj_type == "function" then
return text(show_function(obj, options), colors.lightGrey)
elseif obj_type ~= "table" or tracking[obj] then
return text(tostring(obj), colors.lightGrey)
elseif getmetatable(obj) ~= nil and getmetatable(obj).__tostring then
return text(tostring(obj))
elseif next(obj) == nil then
return braces
else
tracking[obj] = true
local doc = setmetatable({ tag = "concat", n = 1, space_line }, Doc)
local length, keys, keysn = #obj, {}, 1
for k in pairs(obj) do
if type(k) ~= "number" or k % 1 ~= 0 or k < 1 or k > length then
keys[keysn], keysn = k, keysn + 1
end
end
table.sort(keys, key_compare)
for i = 1, length do
if i > 1 then append(doc, comma) append(doc, space_line) end
append(doc, pretty_impl(obj[i], options, tracking))
end
for i = 1, keysn - 1 do
if i > 1 or length >= 1 then append(doc, comma) append(doc, space_line) end
local k = keys[i]
local v = obj[k]
if type(k) == "string" and not keywords[k] and k:match("^[%a_][%a%d_]*$") then
append(doc, text(k .. " = "))
append(doc, pretty_impl(v, options, tracking))
else
append(doc, obracket)
append(doc, pretty_impl(k, options, tracking))
append(doc, cbracket)
append(doc, pretty_impl(v, options, tracking))
end
end
tracking[obj] = nil
return group(concat(obrace, nest(2, concat(table.unpack(doc, 1, doc.n))), space_line, cbrace))
end
end
--- Pretty-print an arbitrary object, converting it into a document.
--
-- This can then be rendered with [`write`] or [`print`].
--
-- @param obj The object to pretty-print.
-- @tparam[opt] { function_args = boolean, function_source = boolean } options
-- Controls how various properties are displayed.
-- - `function_args`: Show the arguments to a function if known (`false` by default).
-- - `function_source`: Show where the function was defined, instead of
-- `function: xxxxxxxx` (`false` by default).
-- @treturn Doc The object formatted as a document.
-- @changed 1.88.0 Added `options` argument.
-- @usage Display a table on the screen
--
-- local pretty = require "cc.pretty"
-- pretty.print(pretty.pretty({ 1, 2, 3 }))
-- @see pretty_print for a shorthand to prettify and print an object.
local function pretty(obj, options)
expect(2, options, "table", "nil")
options = options or {}
local actual_options = {
function_source = field(options, "function_source", "boolean", "nil") or false,
function_args = field(options, "function_args", "boolean", "nil") or false,
}
return pretty_impl(obj, actual_options, {})
end
--[[- A shortcut for calling [`pretty`] and [`print`] together.
@param obj The object to pretty-print.
@tparam[opt] { function_args = boolean, function_source = boolean } options
Controls how various properties are displayed.
- `function_args`: Show the arguments to a function if known (`false` by default).
- `function_source`: Show where the function was defined, instead of
`function: xxxxxxxx` (`false` by default).
@tparam[opt=0.6] number ribbon_frac The maximum fraction of the width that we should write in.
@usage Display a table on the screen.
local pretty = require "cc.pretty"
pretty.pretty_print({ 1, 2, 3 })
@see pretty
@see print
@since 1.99
]]
local function pretty_print(obj, options, ribbon_frac)
expect(2, options, "table", "nil")
options = options or {}
expect(3, ribbon_frac, "number", "nil")
return print(pretty(obj, options), ribbon_frac)
end
return {
empty = empty,
space = space,
line = line,
space_line = space_line,
text = text,
concat = concat,
nest = nest,
group = group,
write = write,
print = print,
render = render,
pretty = pretty,
pretty_print = pretty_print,
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,260 @@
-- asteroids game
-- written by maple "mavica" syrup <maple@maple.pet>
local poly_ship = {{0, -10}, {5, 5}, {0, 0}, {-5, 5}, {0, -10}}
local poly_thrust = {{-2.5, 2,5}, {0, 4}, {2.5, 2.5}}
local poly_shot = {{0, 0}, {0, -5}}
local poly_rock = {{-1,0},{-0.8,-0.6},{-0.4,-0.5},
{0,-1},{0.5,-0.9},{0.3,-0.4},{0.8,-0.5},
{1,0},{0.7,0.4},{0.7,0.6},{0.3,0.8},{0,0.7},
{-0.3,0.9},{-0.7,0.8},{-0.7,0.5},{-1,0}}
local bg = colors.fromRGB(0,0,0)
local fg = colors.fromRGB(0,255,0)
local width = 320
local height = 320
local max_shots = 5
local shot_speed = 7
local rock_speed = 0.6
local rock_size = 30
local rock_step = 10
local player = {
x=width/2,
y=height/2,
accel = 0.06,
vx=0,
vy=0,
ang=0,
cdown=0,
inv=0
}
local shots = {}
local rocks = {}
local score = 0
local lives = 3
local last_frame = 0
local game_over = false
local function sign(number)
return (number > 0 and 1) or (number == 0 and 0) or -1
end
local function distance(x1, y1, x2, y2)
return math.sqrt((x2-x1)^2+(y2-y1)^2)
end
local function draw_rotated(shape, x, y, ang, scale, color)
processed = {}
local sin = math.sin(ang) * scale
local cos = math.cos(ang) * scale
for _,v in pairs(shape) do
local px = x + v[1] * cos - v[2] * sin
local py = y + v[1] * sin + v[2] * cos
table.insert(processed,math.floor(px+0.5))
table.insert(processed,math.floor(py+0.5))
end
draw.polygon(processed, color)
end
local shot_meta = {
erase = function(self)
draw_rotated(poly_shot, self.x, self.y, self.ang, 1, bg)
end,
update = function(self, k)
self.x = self.x + self.vx
self.y = self.y + self.vy
if self.x > width or self.x < 0 or self.y > height or self.y < 0 then
self:erase()
table.remove(shots,k)
else
for rockid,rock in pairs(rocks) do
if distance(self.x, self.y, rock.x, rock.y) < rock.scale then
rock:hit(rockid,self,k)
end
end
end
end,
draw = function(self)
draw_rotated(poly_shot, self.x, self.y, self.ang, 1, fg)
end
}
local rock_meta = {} --hoist
local rock_create = function(x, y, ang, size)
rock = {
x = x,
y = y,
ang = ang,
vx = math.sin(ang) * rock_speed,
vy = -math.cos(ang) * rock_speed,
scale = size
}
setmetatable(rock, { __index = rock_meta })
table.insert(rocks, rock)
end
local function level_init()
for i=1,4 do
rock_create(math.random(30,width-30), math.random(30,height-30), math.random()*6.28, 30)
end
player.inv = 50
end
rock_meta = {
erase = function(self)
draw_rotated(poly_rock, self.x, self.y, self.ang, self.scale, bg)
end,
update = function(self, k)
self.x = (self.x + self.vx) % width
self.y = (self.y + self.vy) % height
if player.inv == 0 and distance(self.x, self.y, player.x, player.y) < self.scale then
player:hit()
end
end,
hit = function(self, k, shot, sk)
sound.playPitch(1, 0.5, sound.drums.snare2)
self:erase()
score = score + ((rock_size + rock_step) - self.scale)
rock_speed = rock_speed + 0.06
if self.scale > rock_step then
rock_create(self.x, self.y, shot.ang - math.random()*3.14, self.scale-rock_step)
rock_create(self.x, self.y, shot.ang + math.random()*3.14, self.scale-rock_step)
end
table.remove(rocks, k)
table.remove(shots, sk)
if #rocks == 0 then
rock_speed = rock_speed - 0.6
score = score + 500
level_init()
end
end,
draw = function(self)
draw_rotated(poly_rock, self.x, self.y, self.ang, self.scale, fg)
end
}
function player:fire()
if self.cdown == 0 and #shots < max_shots then
sound.playPitch(1, 2, sound.drums.tom)
shot = {
x = self.x,
y = self.y,
ang = self.ang,
vx = math.sin(self.ang) * shot_speed,
vy = -math.cos(self.ang) * shot_speed,
}
setmetatable(shot, { __index = shot_meta })
table.insert(shots, shot)
self.cdown = 8
end
end
function player:erase()
draw_rotated(poly_ship, self.x, self.y, self.ang, 1, bg)
draw_rotated(poly_thrust, self.x, self.y, self.ang, 1, bg)
end
function player:update()
local angle_d = (keys.getState(keys.right) and 0.15 or 0) - (keys.getState(keys.left) and 0.15 or 0)
self.ang = self.ang + angle_d
if keys.getState(keys.up) then
self.vx = (self.vx + math.sin(self.ang) * self.accel)
self.vy = (self.vy - math.cos(self.ang) * self.accel)
elseif keys.getState(keys.down) then
self.vx = (self.vx - sign(self.vx) * self.accel)
self.vy = (self.vy - sign(self.vy) * self.accel)
end
self.x = (self.x + self.vx) % width
self.y = (self.y + self.vy) % height
if self.cdown > 0 then
self.cdown = self.cdown - 1
else
if keys.getState(keys.enter) then
self:fire()
end
end
if self.inv > 0 then self.inv = self.inv - 1 end
end
function player:hit()
sound.playPitch(1, 0.4, sound.drums.snare1)
if lives == 0 then game_over = true return end
lives = lives - 1
self.x = width/2
self.y = height/2
self.vx = 0
self.vy = 0
self.ang = 0
self.inv = 50
end
function player:draw()
if math.floor(self.inv / 3) % 2 == 0 then
draw_rotated(poly_ship, self.x, self.y, self.ang, 1, fg)
end
if keys.getState(keys.up) then
draw_rotated(poly_thrust, self.x, self.y, self.ang, 1, fg)
end
end
local function hud_draw()
draw.text(15,15," ",fg,bg)
draw.text(15,15,score,fg,bg)
for i = 0, 3 do
local color = i < lives and fg or bg
draw_rotated(poly_ship, width - 20 - 20*i, 20, 0, 1, color)
end
end
term.clear()
--draw.enableBuffer(true)
draw.rectFill(0, 0, width, height, bg)
level_init()
while true do
if keys.getState(keys.esc) or game_over then break end
local now = os.clock()
if now >= last_frame + 0.05 then
last_frame = now
-- erase
player:erase()
for _,v in pairs(shots) do v:erase() end
for _,v in pairs(rocks) do v:erase() end
-- move player
player:update()
for k,v in pairs(shots) do v:update(k) end
for k,v in pairs(rocks) do v:update(k) end
--draw.clear()
hud_draw()
player:draw()
for _,v in pairs(shots) do v:draw() end
for _,v in pairs(rocks) do v:draw() end
--draw.blitBuffer()
collectgarbage()
end
end
draw.enableBuffer(false)
if game_over then
term.clear()
print("game over!")
print("final score: "..score)
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,448 @@
-- BOXWORLD
-- original game and sprites by Jeng-Long Jiang, 1992
-- picocalc lua conversion by maple "mavica" syrup, 2025
local sprite_size = 20
local sprites = draw.loadBMPSprites("lua/boxworld.bmp", sprite_size, sprite_size)
local fg,bg = colors.fromRGB(255,255,255),colors.fromRGB(0,0,0)
local screen = nil
local quit = false
local function switch_screen(to)
screen = to
if screen.entry then screen.entry() end
end
local music = {
tick = 0.10,
instrument = sound.instrument(sound.presets.square),
notes = {
c = 0,
C = 1,
d = 2,
D = 3,
e = 4,
f = 5,
F = 6,
g = 7,
G = 8,
a = 9,
A = 10,
b = 11
},
tracks = {
intro = {
"l2o3el1<el2gl1el2>cl1<el2>el3dl1c<bafl3>c<bl2al1dl2fl1el2gl1fl2b>c",
"l3o1cga>cel2dl3c<l4bl3fa>cl2<a>c"
},
victory = {
"l2o2el1Gl2al3bl1>dl2ee",
"l3o1aGel1gl2de"
}
},
playing = nil
}
music.instrument.decay = 250
function music.perform()
if not music.playing then return end
local done_playing = true
for k,v in pairs(music.playing) do
local ch = music.channels[k]
local track = music.playing[k]
if ch.cursor <= #track then done_playing = false end
if ch.timer > 0 then ch.timer = ch.timer - 1 end
if ch.timer == 0 then
while ch.cursor <= #track do
local input = string.sub(track, ch.cursor, ch.cursor)
ch.cursor = ch.cursor + 1
if input == 'l' then
input = string.sub(track, ch.cursor, ch.cursor)
ch.cursor = ch.cursor + 1
ch.length = tonumber(input)
elseif input == 'o' then
input = string.sub(track, ch.cursor, ch.cursor)
ch.cursor = ch.cursor + 1
ch.octave = tonumber(input)
elseif input == '<' then
ch.octave = ch.octave - 1
elseif input == '>' then
ch.octave = ch.octave + 1
elseif music.notes[input] then
ch.timer = ch.length
sound.play(k-1, music.notes[input] + (ch.octave+2) * 12, music.instrument)
break
end
end
end
end
if done_playing then music.playing = nil sys.stopTimer() end
end
function music.play(track)
music.playing = track
music.channels = {}
for k,v in pairs(track) do
music.channels[k] = {}
music.channels[k].cursor = 1
music.channels[k].length = 4
music.channels[k].octave = 3
music.channels[k].timer = 0
end
sys.repeatTimer(music.tick * 1000, music.perform)
end
local game = {
level = 1,
levelstrings = {
"00111\n00131\n00121111\n11142431\n13246111\n111141\n000131\n000111\n",
"11111\n16221\n124410111\n124210131\n111211131\n011222231\n012221221\n012221111\n011111\n",
"01111111\n0122222111\n1141112221\n1262422421\n1233124211\n113312221\n011111111\n",
"01111\n11221\n16421\n114211\n112421\n134221\n133531\n111111\n",
"011111\n0162111\n0124221\n11121211\n13121221\n13422121\n13222421\n11111111\n",
"0001111111\n1111222221\n1222311121\n12121222211\n12124241321\n12122522121\n12314242121\n1122221212111\n0121113222261\n0122222112221\n0111111111111\n",
"0001111111\n0011221261\n0012221221\n0014242421\n0012411221\n1112421211\n133333221\n111111111\n",
"000111111\n011122221\n1132411211\n1334242261\n1332424211\n111111221\n000001111\n",
"0111111111\n0122112221\n0122242221\n0142111241\n0121333121\n11213331211\n12422422421\n12222212621\n11111111111\n",
"00111111\n00122221\n11144421\n16243321\n12433311\n1111221\n0001111\n",
"011110011111\n112210012221\n124211114221\n122433332421\n112222126211\n01111111111\n",
"0011111\n1112261\n12243211\n12234321\n11125421\n00122211\n0011111\n",
"001111\n001331\n0112311\n0122431\n11242211\n12214421\n12262221\n11111111\n",
"11111111\n12212221\n12433421\n16435211\n12433421\n12212221\n11111111\n",
"0111111\n11222211\n12424421\n13333331\n12442421\n11126111\n001111\n",
"00111111\n0012222111\n0012422221\n1112421121\n1333242221\n1333414211\n1111212421\n0001226221\n0001111111\n",
"111111\n122221\n1244411\n122133111\n112233421\n012622221\n011111111\n",
"0011111111\n0012221321\n0112243331\n0122421531\n1121141211\n1222422421\n1222122221\n1111111621\n0000001111\n",
"01111111\n01333321\n1113334111\n1224142421\n1244221421\n1222212221\n1111262111\n00011111\n",
"1111111\n1334331\n1331331\n1244421\n1224221\n1244421\n1221621\n1111111\n",
"000111111\n000123331\n111133331\n12211142111\n12424224421\n16242422221\n12221112221\n11111011111\n",
"11111111\n12222221\n12144221\n12333121\n113334211\n012112421\n014224221\n012212261\n011111111\n",
"0011111\n1112221111\n1222424221\n1242224261\n1114411111\n00122331\n00133331\n00111111\n",
"11111100011111\n12222111012231\n12242421013331\n12122421112231\n12244422242631\n11122422412231\n00122414213331\n00112222212231\n00011111111111\n",
"00000111111\n01111132221\n01221331121\n01224332221\n01221231211\n11121141221\n12422224421\n12141221221\n16221111111\n11111\n",
"0111111111\n0122211221111\n0124222222221\n0114111211221\n0122112521211\n012433333321\n112111232121\n122222411141\n122212222461\n111114121111\n000012221\n000011111\n",
"000000111111111\n000000122222221\n000000121212121\n000000122424121\n111111122242221\n133122112424121\n133222112424221\n133122112111111\n133121242421\n133222224221\n122111262111\n1111011111\n",
"00001111\n11111221\n1224242101111111\n1222422101535351\n1124242111353531\n0142422122535351\n0164242222353511\n0142422122535351\n1124242111353531\n1222422101535351\n1224242101111111\n11111221\n00001111\n",
"11111111\n13333331\n122421211\n124212421\n114242421\n012262221\n011111111\n",
"001111111111\n111222322221\n122211411221\n126432323411\n11241141121\n01222232221\n01111111111\n",
"000111111\n111132261\n122444221\n131131131\n122242221\n122431211\n11112221\n00011111\n",
"0111111\n0132331\n0132431\n11122411\n12422421\n12141121\n12226221\n11111111\n",
"0000111111\n001112222111\n00122214222111\n00122242224421\n00124421422221\n00112224222421\n11111121411111\n1336214221\n1313322411\n133334121\n133332221\n111111111\n",
"111111111111111\n122222212222221\n124214212411421\n121224212222221\n122211414114421\n121212333212221\n124223212342121\n124164333121221\n122223212322421\n121134111432121\n121243333321121\n122222222222221\n111111111111111\n",
"111111111\n122211221\n121242421\n122531221\n112136311\n1141115111\n1222222221\n1222112121\n1111112221\n0000011111\n",
"11111111\n12222221\n1244222111\n12242444211111\n112112333222211\n012161333111421\n012124333222221\n112124333421211\n12211111211121\n12222224222421\n11111111111221\n00000000001111\n",
"00011111\n00012621\n00014441\n11112221\n122231411\n124343231\n122131311\n11111111\n",
"111111111111\n133321222221\n133221211221\n133222221221\n133221241121\n133321424221\n111111224421\n011224244221\n016244422121\n011242112221\n001222222221\n001111111111\n",
"111111111\n122222221\n122424241\n112141121\n0123323311\n0113323321\n00121141211\n00142424221\n00122222261\n00111111111\n",
"111110000001111\n162211111111221\n112422222224221\n012121221111221\n012242221111411\n01421121242421\n11242241222221\n12221222222121\n12221111141111\n1111122212221\n0000133322421\n0000133331221\n0000133331111\n0000111111\n",
"0000011111\n0111112221\n0123324121\n0121352221\n1125314211\n124224221\n122211261\n111111111\n",
"1111101111111\n1222111221221\n1242222242621\n1121411311221\n0122333532421\n0124121312121\n0112222422221\n0012211111111\n001111\n",
"000000000111\n0000111111611\n00001333314211\n00001333312421\n00001333324221\n00001233312221\n111111211111211\n124242224221221\n122224422242421\n111242424221111\n001122242421\n000122111111\n0001111\n",
"00011111\n0111222111\n1122642421\n12211211211\n12431342221\n12131512221\n12433322111\n111412111\n0012221\n0011111\n",
"0000001111\n000000122111111\n000000122221221\n000000124422221\n111111141221221\n122132332111411\n122131534222221\n122131351212221\n124433331211111\n12642121121\n12444122221\n12222111111\n111111\n",
"1111\n122111\n12422111\n1242422111\n124242422111\n124242422221\n1242422122211\n1242211244421\n16211112222211\n11210134444431\n012111333333311\n012223555555531\n011113333333331\n000011111111111\n",
"1111111\n12612211111\n12442242221\n12213114121\n11413332221\n11233311411\n12211311221\n12242242221\n12212221221\n11111111111\n",
"00011111\n00012621\n00012421\n00014341\n011134311\n11234343111\n12243434221\n12222322221\n11111111111\n",
"1111111111111\n1224242435331\n1242424253331\n1224242435331\n1242424253331\n1224242435331\n1242424253331\n1224242435331\n1242424253331\n1224242435331\n1642424253331\n1111111111111\n",
"00000000000001\n00000000000011\n00000000000111\n000000000012221\n000111111112121\n001242424242221\n01121313131641\n111333333322211\n01121212121411\n001242424242221\n000111111112121\n000000000012221\n00000000000111\n00000000000011\n",
"1111111\n122342111\n123434221\n154343621\n123434211\n12234221\n11111111\n",
"00000000011111\n00000000012221\n1111111111252111\n1222222222232221\n1244445555433361\n1222222222232221\n1111111111252111\n00000000012221\n00000000011111\n",
"000001111\n1111112211111\n1642222422421\n1411124212121\n1221221242221\n1241222212111\n122421412221\n133333333321\n111111112221\n000000011111\n",
"0111111111\n0122211221111\n0124222222221\n0114111211221\n0122113421211\n012433333321\n112111322121\n122222411141\n122212222461\n111114121111\n000012221\n000011111\n",
"11111111111\n12222122221\n12464444421\n12222222221\n11111211111\n00013221\n00013221\n00013331\n00013221\n00011111\n",
"0011111\n0012621\n0012421\n111232111\n122252221\n125555521\n122252221\n111454111\n0012321\n0012521\n0012321\n0011111\n",
"11111111111111\n13222222222221\n13424242424221\n13111111111221\n13135242334511\n1312424253461\n1313224233441\n1311111111131\n1322222222221\n1314141414141\n1322222222221\n1111111111111\n",
"111111111111\n13322122222111\n13322124224221\n13322141111221\n13322226211221\n13322121224211\n11111121142421\n00124224242421\n00122221222221\n00111111111111\n",
"00000001111\n11111111221111\n12221133333221\n12242211333121\n11224221112121\n01212422122221\n01221242212221\n01222124221221\n01222212421211\n0111122124221\n0000112212421\n0000011612221\n0000001111111\n",
"11110000001111\n13311111111331\n15353333353531\n12424242424241\n14242464242421\n12424242424241\n14242424242421\n13535333335351\n13311111111331\n11110000001111\n",
"000011111\n000112221111\n000123352421\n111121312221\n122223531611\n12141141121\n12222242421\n11221222111\n011111111\n",
"111111\n122221\n124221111\n124533521\n125335421\n111122421\n000126221\n000111111\n",
"00011111\n111132211\n124343221\n164121421\n124323221\n111141421\n001323221\n001111111\n",
"111111111111\n122223332421\n124445552461\n122223332421\n111111111111\n",
"1111111111\n1122222221\n1222141421\n1244223431\n1261113331\n1111111111\n",
"01111\n012211111\n114211221\n122464221\n122211421\n1113112111\n0133342421\n0113322221\n0011111111\n",
"1111101111\n1333101221111\n1333111224221\n133331124224111\n113333112224221\n111333211242421\n121122221224221\n1221121211121111\n1242121422422221\n1224262422224221\n1222124244242111\n12211111122111\n121100001111\n111\n",
"1111111\n1323231\n1244421\n1346431\n1244421\n1323231\n1111111\n",
"0000001111\n1111111221\n1222224221\n1222411641\n1141333121\n0124333221\n01213231211\n01222121421\n01422422221\n01221111111\n01111\n",
"000111111111\n000122212221\n000122222221\n111115111211\n12223332221\n121215111411\n124222242221\n111116212221\n000011111111\n",
"11111\n13331011111\n13331112221\n133332224411111\n1333322122122211\n1331411112141221\n1124221222224421\n1224126242441221\n1242424212224211\n122212242112221\n111111222111111\n0000011111\n",
"111111111111111\n111312222221111\n113312422421221\n133312112421221\n133333221442221\n113333422221421\n111121111111221\n122242222222211\n122421224124211\n124111242124421\n122261221122221\n111111111111111\n",
"000000011111111\n000000012212221\n011111112443331\n012222222213331\n012111111413331\n112122222213331\n122121424211111\n12124242421\n12622421221\n11111424421\n00001222221\n00001111111\n",
"000000011111\n001111112221\n111222232421\n124221431411\n122122631221\n112111132221\n012422151111\n01211213221\n01222223121\n01114222221\n00012211111\n0001111\n",
"00001111\n111112211111\n122224222421\n122414112221\n111213532111\n0012333261\n0011214111\n00012221\n00011111\n",
"00011111111\n11112222321\n12242424321\n12231111311\n1243424261\n1223221111\n1111111\n",
"0011111\n0012221\n11143411111\n12223242221\n12114112621\n12223211111\n1112321\n0012221\n0011111\n",
"011111\n01262111111\n01213352221\n01233312221\n11411242421\n12221411111\n122242221\n111112121\n000012221\n000011111\n",
"00000111111\n000111222211\n000122211221\n011141122121\n112222233121\n122414153121\n12446213512111\n12244213312221\n11222213342221\n01114113212111\n000122111221\n000112222211\n00001111111\n",
"000001111111\n000001221221\n000001224421\n111111241221\n1333111212211\n1322122421221\n1322224242421\n1322122421221\n1333111212211\n111111242221\n000001621221\n000001111111\n",
"0000000000111111\n0000000000122221\n1111100011121121\n1332111112422121\n1332222242224121\n1332211211222121\n1332112421424121\n1332122222422121\n1332122421114221\n1332124242242111\n111211212422221\n001222216112421\n001111111112211\n00000000001111\n",
"00000001\n0000011111\n000111262111\n000122424221\n000125353521\n0011234243211\n01112535352111\n111122424221111\n000111111221\n00001100011\n",
"011111111111111\n0126252525212211\n0141225252212221\n0121252525222221\n0121225252211211\n012125252521121\n012122525221121\n012125252521121\n012122525221121\n0121252325211211\n1121111111111221\n1222222222222221\n1222111111111221\n1111100000001111\n",
"01111111111111\n01222212211221\n014442124422411\n012422122333321\n012242214311321\n012212412333311\n114242214311321\n124224264333321\n122211122111111\n1111101111\n",
"00000111111111\n00000122222221\n00000122414121\n01111112212421\n01222124224221\n11242222211121\n12221411112221\n12222421112111\n1111133261211\n0001333424421\n0001333122221\n0001333111111\n00011111\n",
"1111000001111\n122111111133111\n124242422133331\n124222442155531\n124242422133531\n122424242153531\n1124242423535311\n1224242423535361\n1224242421535311\n124242422133531\n124222442155531\n124242422133331\n122111111133111\n1111000001111\n",
"00000011111\n11111112221\n12221132221\n122413322111\n1122333144211111\n0124313422222221\n0124111411212421\n0122212222244121\n0114412114142221\n0133324624222111\n01333141222111\n013332211111\n01111111\n",
"0111100111111\n0122111122221\n1152225255221\n1242522225121\n1232221112221\n1111112221611\n1252325225521\n1222122212221\n1152225214121\n0122111112221\n0111100011111\n",
"0111111111\n0122212221\n0124444421\n1124242421\n1242262221\n12421111211\n12213333321\n11223333321\n01111111111\n",
"0001111111\n0001222221\n00012424211\n000111113311111\n111111335322421\n122464333314421\n122242141112221\n111112222222111\n0000111221111\n0000001221\n0000001111\n",
"11111\n1222111111\n1242332421\n1142334461\n0122332421\n0111111111\n",
"0011111\n001222111111\n111413222221\n124233312421\n162431542221\n111122221111\n000111111\n",
"000011111\n111112221\n122242621\n122421311111\n114211311221111\n012233333241221\n012411311221421\n012221311222221\n011124211111211\n000121422222421\n000122221112221\n000111111011111\n",
"1111111\n1225221\n1263421\n1243221\n1535551\n1225221\n1245421\n1223221\n1111111\n",
"00111111111\n00122221221\n11121422421111\n1224221133122111\n1212242133424221\n1244224133221221\n1122122333142221\n0142621333124221\n0122221333422211\n012114211122211\n01222422222111\n012211111111\n01111\n",
"0001111111111\n1111333333221\n1222333331221\n1221333333211\n112111141141\n16422424222111\n12442222112221\n12122441122121\n12224221244221\n12242242221421\n11112221242421\n00012221222221\n00011111111111\n",
"000000111111111\n000000122222221\n000000142444221\n000000122212421\n000000122246421\n000011142421211\n00001222414121\n11111212212221\n13332212412111\n133333222121\n133333124121\n111111112221\n000000011111\n",
"0111110111111\n0122211122221\n1124242142141\n12242624224211\n12122112133331\n12211242131131\n11224222233331\n01244214133331\n01222122214211\n0111112422221\n0000011112211\n000000001111\n",
"1111\n1221\n1221111111111\n1222211222221\n1331222244121\n133221122242111\n133122114124221\n133222126424221\n133122124242221\n123222124242111\n1221221222111\n12212222111\n111111111\n",
"00000000011111\n000000001222221\n0000000122411221\n0000001221222421\n1001001212212121\n0101001242422421\n0011111112141221\n001224242222221\n00163342553111\n000133333311111\n0000111111111111\n"
},
anim_timer = 0,
player_flip = 0,
player_sprites = {
U = {7, 8},
D = {5, 6},
L = {11, 12},
R = {9, 10},
W = {13, 13}
}
}
game.push_sound = sound.instrument(sound.drums.tom)
game.push_sound.volume = 0.5
game.push_sound.decay = 150
game.push_sound.sustain = 0
local title = {}
function title.input()
local state, _, code = keys.poll()
if state == keys.states.pressed then
if code == keys.left then
game.level = ((game.level - 2) % #game.levelstrings) + 1
screen.redraw = true
elseif code == keys.right then
game.level = ((game.level) % #game.levelstrings) + 1
screen.redraw = true
elseif code == keys.enter then
switch_screen(game)
elseif code == keys.esc then
quit = true
end
end
end
function title.entry()
draw.enableBuffer(false)
music.play(music.tracks.intro)
draw.clear()
screen.redraw = true
end
function title.draw()
-- boxworld logo
for j = 0, 1 do
for i = 0, 6 do
sprites:blit(160 - 70 + i * sprite_size, 80 + j * sprite_size, 14 + i + j * 7)
end
end
draw.text(160, 140, "Original by Jeng-Long Jiang, 1992",fg,bg,draw.align_center)
draw.text(160, 160, "Reprogram by maple \"mavica\" syrup, 2025",fg,bg,draw.align_center)
draw.text(160, 180, "Select level",fg,bg,draw.align_center)
draw.text(160, 190, " < " .. game.level .. " > ",fg,bg,draw.align_center)
end
function game.entry()
-- load level
game.leveltable = {}
local row = {}
local longest = 0
local str = game.levelstrings[game.level]
for i = 1, #str do
if str:sub(i,i) == '\n' then
table.insert(game.leveltable, row)
if #row > longest then longest = #row end
row = {}
elseif str:sub(i,i) == '6' then -- player
game.player_pos = {#row + 1, #game.leveltable + 1}
game.player_direction = "D"
table.insert(row, '2') -- empty floor under player
else
table.insert(row, str:sub(i,i))
end
end
game.draw_offset = {140 - longest/2*sprite_size, 140 - #game.leveltable/2*sprite_size}
game.moves = {}
collectgarbage()
draw.enableBuffer(1)
draw.clear()
screen.redraw = true
end
function game.draw()
for j = 1, #game.leveltable do
local row = game.leveltable[j]
for i = 1, #row do
local pos = {game.draw_offset[1] + i * sprite_size, game.draw_offset[2] + j * sprite_size}
if row[i] == "1" then -- wall
sprites:blit(pos[1], pos[2], 0)
elseif row[i] == "2" then -- floor
sprites:blit(pos[1], pos[2], 1)
elseif row[i] == "3" then -- floor with goal
sprites:blit(pos[1], pos[2], 1)
sprites:blit(pos[1], pos[2], 4)
elseif row[i] == "4" then -- box
sprites:blit(pos[1], pos[2], 2)
elseif row[i] == "5" then -- box on goal
sprites:blit(pos[1], pos[2], 3)
end
end
end
local pos = {
game.draw_offset[1] + game.player_pos[1] * sprite_size,
game.draw_offset[2] + game.player_pos[2] * sprite_size
}
sprites:blit(pos[1], pos[2], game.player_sprites[game.player_direction][game.player_flip+1])
if game.player_direction == "W" then
draw.text(160, 160, "You win !!!",fg,bg,draw.align_center)
draw.text(160, 170, "Press ENTER for next level",fg,bg,draw.align_center)
end
draw.text(0, 300, "Box World! #" .. game.level .. " - Moves: " .. #game.moves .. " ")
draw.text(0, 310, "(U)ndo, (R)estart, (ESC) Quit")
draw.blitBuffer()
end
function game.check_win()
for j = 1, #game.leveltable do
local row = game.leveltable[j]
for i = 1, #row do
if row[i] == "4" then -- box not on goal
return false -- quit as soon as found any
end
end
end
-- didn't quit? must mean we won
game.player_direction = "W"
music.play(music.tracks.victory)
end
function game.process_push(code)
if game.player_direction == "W" then return end -- don't move if we've already won
local ahead, ahead2
if code == keys.up then
game.player_direction = "U"
ahead = {game.player_pos[1], game.player_pos[2]-1}
ahead2 = {game.player_pos[1], game.player_pos[2]-2}
elseif code == keys.down then
game.player_direction = "D"
ahead = {game.player_pos[1], game.player_pos[2]+1}
ahead2 = {game.player_pos[1], game.player_pos[2]+2}
elseif code == keys.left then
game.player_direction = "L"
ahead = {game.player_pos[1]-1, game.player_pos[2]}
ahead2 = {game.player_pos[1]-2, game.player_pos[2]}
elseif code == keys.right then
game.player_direction = "R"
ahead = {game.player_pos[1]+1, game.player_pos[2]}
ahead2 = {game.player_pos[1]+2, game.player_pos[2]}
end
local whats_ahead = game.leveltable[ahead[2]]
if whats_ahead ~= nil then whats_ahead = whats_ahead[ahead[1]] end
local whats_ahead2 = game.leveltable[ahead2[2]]
if whats_ahead2 ~= nil then whats_ahead2 = whats_ahead2[ahead2[1]] end
if whats_ahead == '2' or whats_ahead == '3' then -- ahead is floor or empty goal
game.player_pos = ahead
table.insert(game.moves, string.lower(game.player_direction))
elseif whats_ahead == '4' or whats_ahead == '5' then -- ahead is box, whether or not on a goal
if whats_ahead2 == '2' or whats_ahead2 == '3' then -- box pushed onto floor or goal
sound.playPitch(2,1,game.push_sound)
local box_leaves = whats_ahead == '4' and '2' or '3'
local box_becomes = whats_ahead2 == '2' and '4' or '5'
game.player_pos = ahead
game.leveltable[ahead[2]][ahead[1]] = box_leaves
game.leveltable[ahead2[2]][ahead2[1]] = box_becomes
table.insert(game.moves, game.player_direction)
game.check_win()
end
end
screen.redraw = true
end
function game.process_undo()
if #game.moves < 1 then return false end
local return_to, pushed
local move = table.remove(game.moves)
game.player_direction = string.upper(move)
if string.upper(move) == 'D' then
return_to = {game.player_pos[1], game.player_pos[2]-1}
pushed = {game.player_pos[1], game.player_pos[2]+1}
elseif string.upper(move) == 'U' then
return_to = {game.player_pos[1], game.player_pos[2]+1}
pushed = {game.player_pos[1], game.player_pos[2]-1}
elseif string.upper(move) == 'R' then
return_to = {game.player_pos[1]-1, game.player_pos[2]}
pushed = {game.player_pos[1]+1, game.player_pos[2]}
elseif string.upper(move) == 'L' then
return_to = {game.player_pos[1]+1, game.player_pos[2]}
pushed = {game.player_pos[1]-1, game.player_pos[2]}
end
if move == string.upper(move) then -- if move was recorded upper means we moved a box
local what_box_became = game.leveltable[pushed[2]][pushed[1]]
local box_leaves = what_box_became == '4' and '2' or '3'
local box_becomes = game.leveltable[game.player_pos[2]][game.player_pos[1]] == '2' and '4' or '5'
game.leveltable[pushed[2]][pushed[1]] = box_leaves
game.leveltable[game.player_pos[2]][game.player_pos[1]] = box_becomes
end
game.player_pos = return_to
screen.redraw = true
end
function game.input()
local state, _, code = keys.poll()
if state == keys.states.pressed then
if code == keys.up or code == keys.down or code == keys.left or code == keys.right then
game.process_push(code)
elseif code == 'u' then
game.process_undo()
elseif code == 'r' then
game.entry()
elseif code == keys.esc then
switch_screen(title)
elseif game.player_direction == "W" and code == keys.enter then
-- go to next level when won and pressed enter
game.level = ((game.level) % #game.levelstrings) + 1
game.entry()
end
end
collectgarbage()
end
function game.update(now)
if game.player_direction ~= 'W' and now > game.anim_timer then
game.player_flip = (game.player_flip + 1) % 2
game.anim_timer = now + 1
screen.redraw = true
end
end
switch_screen(title)
while true do
now = os.clock()
if screen.input then screen.input() end
if quit then break end
if screen.update then screen.update(now) end
if screen.redraw and screen.draw then screen.draw() screen.redraw = false end
end

View File

@@ -0,0 +1,74 @@
local current_dir = "/"
local cursor_y = 1
local width, height = term.getSize()
function string:endswith(suffix)
return self:sub(-#suffix) == suffix
end
term.clear()
while true do
local dir = fs.list(current_dir)
term.setCursorPos(1,1)
table.sort(dir, function(a, b) return string.upper(a.name) < string.upper(b.name) end)
local folders = {}
local files = {}
for _,v in pairs(dir) do
if v.isDir then table.insert(folders, v)
else table.insert(files, v) end
end
table.sort(files, function(a, b) return a.name < b.name end)
dir = folders
for _,v in ipairs(files) do
table.insert(dir, v)
end
if current_dir > "/" then
table.insert(dir, 1, {name="..", isDir=true})
end
local off = 0
if #dir > height-2 then
off = math.floor(cursor_y / #dir * (#dir - height + 3))
end
for y = 1, height - 2 do
local yoff = y + off
local v = dir[yoff]
if v then
local str = ""
if v.isDir then str = str .. "\27[93m"
elseif v.name:endswith(".lua") then str = str .. "\27[96m"
else str = str .. "\27[97m" end
if yoff == cursor_y then str = str .. "\27[7m" end
term.write(str .. v.name .. "\27[m\27[K\n")
else
term.write("\27[K\n")
end
end
term.write("\n\27[93mCurrent directory: " .. current_dir .. "\27[m\27[K")
local state, _, code = keys.wait(true, true)
if code == keys.up then
cursor_y = ((cursor_y - 2) % #dir) + 1
elseif code == keys.down then
cursor_y = ((cursor_y) % #dir) + 1
elseif code == keys.enter then
if dir[cursor_y].isDir then
if dir[cursor_y].name == ".." then
current_dir = current_dir:match("(.*/).*/") or '/'
else
current_dir = current_dir .. dir[cursor_y].name .. '/'
end
cursor_y = 1
elseif dir[cursor_y].name:endswith(".lua") then
edit(current_dir .. dir[cursor_y].name)
end
elseif code == keys.esc then
break
end
end
term.clear()

View File

@@ -0,0 +1,50 @@
-- Bubble Universe
-- inspired by https://forum.clockworkpi.com/t/bubble-universe/19907
local w = 320
local w2 = w/2
local rad = 70
local n = 20
local nc = 120
local step = 2
local r = math.pi*2/n
local x,y,t = 0,0,6
local u,v,px,py
local res = 100
local sint = {}
for i = 1, math.floor(math.pi*2*res) do
sint[i] = math.sin(i/res)
end
local function fsin(i)
return sint[(math.floor(i*res)%#sint)+1]
end
local function fcos(i)
return sint[((math.floor((math.pi/2-i)*res))%#sint)+1]
end
draw.enableBuffer(2)
while true do
if keys.getState(keys.esc) then break end
draw.clear()
for i = 0, n-1 do
for c = 0, nc-1, step do
u = fsin(i+y)+fsin(r*i+x)
v = fcos(i+y)+fcos(r*i+x)
x = u + t
y = v
px = u * rad + w2
py = y * rad + w2
draw.point(px, py,
--draw.rectFill(px, py, 2, 2,
colors.fromRGB(math.floor(63+i/n*192),
math.floor(63+c/nc*192),168))
end
end
t=t+0.02
draw.blitBuffer()
end
draw.enableBuffer(false)

View File

@@ -0,0 +1,107 @@
-- partially adapted from https://bisqwit.iki.fi/jutut/kuvat/programming_examples/mandelbrotbtrace.pdf
-- arrow keys, 9 and 0 to control view, escape to quit
local maxIter = 64
local chunk = 8
local center = {-1,0}
local radius = 1
local minX, maxX, minY, maxY
local wid, hei = 320, 320
local function iterate(zr, zi, max)
local cnt, r, i, r2, i2, ri
cnt = 0
r,i = zr,zi
while cnt < max do
r2 = r*r; i2 = i*i
if r2+i2 >= 4 then break end
ri = r*i
i = ri+ri + zi
r = r2-i2 + zr
cnt = cnt + 1
end
return cnt
end
local function is_control_key()
local state, _, code = keys.peek()
if state == keys.states.pressed then
if code == keys.up
or code == keys.down
or code == keys.left
or code == keys.right
or code == '9'
or code == '0'
or code == '['
or code == ']'
or code == keys.esc then
return true
end
end
keys.flush()
return false
end
local function drawScanlineMandelbrot(max)
local zr, zi, cnt, clr
local st = chunk
local proc = {}
local stepR = (maxX - minX) / wid
local stepI = (maxY - minY) / hei
while st >= 1 do
for y = 0, hei - 1, st do
zi = minY + y * stepI
if proc[(y % (st*2))] then goto next end
for x = 0, wid - 1 do
zr = minX + x * stepR
cnt = iterate(zr, zi, max)
if cnt == max then clr = 0
else
cnt = math.floor(cnt/max*255)
clr = colors.fromHSV((-cnt-32)%256,255,127+math.floor(cnt/2))
end
draw.line(x, y, x, y+st-1, clr)
if is_control_key() then return end
end
::next::
end
proc[st%chunk] = true
st = math.floor(st/2)
end
end
while true do
minX, maxX, minY, maxY = center[1]-radius, center[1]+radius, center[2]-radius, center[2]+radius
drawScanlineMandelbrot(maxIter)
while true do
local _, _, code = keys.wait(false, true)
if code == keys.up then
center[2] = center[2] - radius * 0.25
break
elseif code == keys.down then
center[2] = center[2] + radius * 0.25
break
elseif code == keys.left then
center[1] = center[1] - radius * 0.25
break
elseif code == keys.right then
center[1] = center[1] + radius * 0.25
break
elseif code == '9' then
radius = radius * 4
break
elseif code == '0' then
radius = radius * 0.25
break
elseif code == '[' then
maxIter = maxIter - 10
break
elseif code == ']' then
maxIter = maxIter + 10
break
elseif code == keys.esc then
goto exit
end
end
end
::exit::

View File

@@ -0,0 +1,84 @@
-- kalimba / piano demo
-- written by maple "mavica" syrup <maple@maple.pet>
local pkeys = {}
local keymap = {
"5","6","4","7","3","8","2","9",
"t","y","r","u","e","i","w","o",
"g","h","f","j","d","k","s","l",
"b","n","v","m","c",",","x","."
}
local qwertymap = {
"2","3","4","5","6","7","8","9",
"w","e","r","t","y","u","i","o",
"s","d","f","g","h","j","k","l",
"x","c","v","b","n","m",",","."
}
for k,v in pairs(keymap) do
pkeys[v] = k - 1
end
local last = nil
local ch = 0
local octave = 3
local inst_num = 0
local insts = {}
local inst_names = {}
for k,v in pairs(sound.presets) do
table.insert(insts, v)
table.insert(inst_names, k)
end
local function noteString(note)
local notes = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"}
return notes[(note % 12)+1] .. math.floor(note / 12)
end
local function redraw()
term.setCursorPos(1,1)
term.write("Piano / Kalimba demo\n\n")
for j = 0, 24, 8 do
for i = 1, 8 do
if qwertymap[i+j] == last then term.write("\27[7m") end
term.write(qwertymap[i+j].."\27[m ")
end
term.write("\n")
end
term.write("\n")
if pkeys[last] then term.write(noteString(pkeys[last] + octave * 12)) end
term.write("\n\n")
term.write("Octave: " .. octave .. " | Instrument: " .. inst_names[inst_num+1] .. "\n\x1b[K")
term.write("/ \\ - Change octave\n[ ] - Change instrument\nEsc - Quit\n")
end
term.clear()
while true do
redraw()
local state, _, code = keys.wait(false, false)
if state == keys.states.pressed and code ~= last then
if pkeys[code] then
last = code
sound.play(ch, pkeys[code] + octave * 12, insts[inst_num+1])
ch = (ch+1) % 4
elseif code == "[" then
inst_num = (inst_num - 1) % #insts
elseif code == "]" then
inst_num = (inst_num + 1) % #insts
elseif code == "/" and octave > 0 then
octave = (octave - 1)
elseif code == "\\" and octave < 6 then
octave = (octave + 1)
elseif code == keys.esc then
break
end
elseif state == keys.states.released then
last = nil
end
end

4
Bin/PicoCalc SD/main.lua Normal file
View File

@@ -0,0 +1,4 @@
--ccedit = loadfile("cc/edit.lua")
--if ccedit then print("Editor loaded") end
browser = loadfile("lua/browser.lua")
if browser then print("File browser available at: browser()") end

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More