diff --git a/display.lisp b/display.lisp new file mode 100644 index 0000000..52e3e69 --- /dev/null +++ b/display.lisp @@ -0,0 +1,124 @@ +;;;; Copyright © 2023, Jaidyn Ann +;;;; +;;;; This program is free software: you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License as +;;;; published by the Free Software Foundation, either version 3 of +;;;; the License, or (at your option) any later version. +;;;; +;;;; 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. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program. If not, see . + +;;;; FLORA-SEARCH-AURORA.DISPLAY +;;;; All display-related curses go here. + +(defpackage :flora-search-aurora.display + (:use :cl) + (:export #:make-screen-matrix #:print-screen-matrix #:screen-matrix-set-map)) + +(in-package :flora-search-aurora.display) + + +(defmacro do-for-cell (matrix &body body) + "Given a 2d-array (matrix), execute the body for every cell. +The body has access to 4 variables: + * i/j — The current row/column. + * dimensions — Dimensions of the given matrix. + * cell — The value of the current cell." + `(let* ((dimensions (array-dimensions ,matrix)) + (max-i (car dimensions)) + (max-j (cadr dimensions)) + (i 0) (j 0)) + (loop + (let ((cell (ignore-errors (aref ,matrix i j)))) + (cond + ((< i max-i) + (cond + ((< j max-j) + ,@body + (incf j)) + ((eq j max-j) + (setf j 0) + (incf i)))) + ((eq i max-i) + (return))))))) + + +(defun matrix-delta (a b) + "Given two 2D matrices, return a matrix containing only the cells +that change between a→b (favouring those in b) — all others are nil." + (let ((delta (make-array (array-dimensions a)))) + (do-for-cell a + (when (not (eq cell + (aref b i j))) + (setf (aref delta i j) + (aref b i j)))) + delta)) + + +(defun print-screen-matrix (matrix) + "Given a matrix of characters, print each element to standard output." + (do-for-cell matrix + (when (characterp cell) + (move-cursor (+ i 1) (+ j 1)) + (write-char cell)))) + + +(defun make-screen-matrix () + "Create a “screen matrix” — that is, a 2D array representing the +72x20 grid of characters we can print to the terminal." + (make-array '(20 72) :initial-element #\space)) + + +(defun screen-matrix-set-char-cell (matrix cell) + "Set a matrice's (2d array's) element corresponding to +a Tiled cell's character-value, using it's column and row." + (setf (aref matrix + (cl-tiled:cell-row cell) + (cl-tiled:cell-column cell)) + (tile-character + (cl-tiled:cell-tile cell)))) + + +(defun screen-matrix-set-map (matrix map-path) + "Draw a Tiled-format tilemap to the 2D array." + (mapcar (lambda (layer) (screen-matrix-set-map-layer matrix layer)) + (cl-tiled:map-layers (cl-tiled:load-map map-path))) + matrix) + + +(defun screen-matrix-set-map-layer (matrix tile-layer) + "Set an array's elements to those corresponding the given Tiled +tile-layer's cells. a Tiled tile-layer to the screen." + (mapcar (lambda (cell) (screen-matrix-set-char-cell matrix cell)) + (cl-tiled:layer-cells tile-layer)) + matrix) + + +(defun tile-character (tile) + "Given a tileset's tile, return it's corresponding text character, +assuming that the tileset is a bitmap font starting with char-code 32 +with 15 characters-per-line." + (code-char + (+ (* (cl-tiled:tile-row tile) 15) + (cl-tiled:tile-column tile) + 32))) + + +;;; ~ Utilities ~ + +(defun move-cursor (row column &key (stream *standard-output*)) + "Moves cursor to desired position. +Borrowed from https://github.com/gorozhin/chlorophyll/ +Copyright © 2022 Mikhail Gorozhin — MIT license" + (format stream "~C[~A;~AH" #\Esc row column)) + + +(defun clear-screen (&key (stream *standard-output*)) + "Completely clear the terminal screen." + (move-cursor 0 0 :stream stream) + (format stream "~C[J" #\Esc)) diff --git a/flora-search-aurora.lisp b/flora-search-aurora.lisp index f47bc14..6dd1874 100644 --- a/flora-search-aurora.lisp +++ b/flora-search-aurora.lisp @@ -1,182 +1,31 @@ -;;;; A simple TUI-game made for the text-flavoured LibreJam of 2023-06! -;;;; https://jamgaroo.xyz/jams/2 +;;;; Copyright © 2023, Jaidyn Ann +;;;; +;;;; This program is free software: you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License as +;;;; published by the Free Software Foundation, either version 3 of +;;;; the License, or (at your option) any later version. +;;;; +;;;; 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. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program. If not, see . + +;; FLORA-SEARCH-AURORA +;; A simple TUI-game made for the text-flavoured LibreJam of 2023-06! +;; See: https://jamgaroo.xyz/jams/2 (ql:quickload '(alexandria cl-charms cl-tiled str)) +(load "input.lisp") +(load "display.lisp") +(defpackage :flora-search-aurora + (:use :cl :flora-search-aurora.input :flora-search-aurora.display) + (:export #:main)) -(defun move-cursor (row column &key (stream *standard-output*)) - "Moves cursor to desired position. -Borrowed from https://github.com/gorozhin/chlorophyll/ -© 2022 Mikhail Gorozhin — MIT license" - (format stream "~C[~A;~AH" #\Esc row column)) - - -(defun clear-screen (&key (stream *standard-output*)) - "Completely clear the terminal screen." - (move-cursor 0 0 :stream stream) - (format stream "~C[J" #\Esc)) - - -(defmacro do-for-cell (matrix &body body) - "Given a 2d-array (matrix), execute the body for every cell. -The body has access to 4 variables: - * i/j — The current row/column. - * dimensions — Dimensions of the given matrix. - * cell — The value of the current cell." - `(let* ((dimensions (array-dimensions ,matrix)) - (max-i (car dimensions)) - (max-j (cadr dimensions)) - (i 0) (j 0)) - (loop - (let ((cell (ignore-errors (aref ,matrix i j)))) - (cond - ((< i max-i) - (cond - ((< j max-j) - ,@body - (incf j)) - ((eq j max-j) - (setf j 0) - (incf i)))) - ((eq i max-i) - (return))))))) - - -(defun matrix-delta (a b) - "Given two 2D matrices, return a matrix containing only the cells -that change between a→b (favouring those in b) — all others are nil." - (let ((delta (make-array (array-dimensions a)))) - (do-for-cell a - (when (not (eq cell - (aref b i j))) - (setf (aref delta i j) - (aref b i j)))) - delta)) - - -(defun print-screen-matrix (matrix) - "Given a matrix of characters, print each element to standard output." - (do-for-cell matrix - (when (characterp cell) - (move-cursor (+ i 1) (+ j 1)) - (write-char cell)))) - - -(defun make-screen-matrix () - "Create a “screen matrix” — that is, a 2D array representing the -72x20 grid of characters we can print to the terminal." - (make-array '(20 72) :initial-element #\space)) - - -(defun screen-matrix-set-map (matrix map-path) - "Draw a Tiled-format tilemap to the 2D array." - (mapcar (lambda (layer) (screen-matrix-set-map-layer matrix layer)) - (cl-tiled:map-layers (cl-tiled:load-map map-path))) - matrix) - - -(defun screen-matrix-set-map-layer (matrix tile-layer) - "Set an array's elements to those corresponding the given Tiled -tile-layer's cells. a Tiled tile-layer to the screen." - (mapcar (lambda (cell) (screen-matrix-set-char-cell matrix cell)) - (cl-tiled:layer-cells tile-layer)) - matrix) - - -(defun screen-matrix-set-char-cell (matrix cell) - "Set a matrice's (2d array's) element corresponding to -a Tiled cell's character-value, using it's column and row." - (setf (aref matrix - (cl-tiled:cell-row cell) - (cl-tiled:cell-column cell)) - (tile-character - (cl-tiled:cell-tile cell)))) - - -(defun tile-character (tile) - "Given a tileset's tile, return it's corresponding text character, -assuming that the tileset is a bitmap font starting with char-code 32 -with 15 characters-per-line." - (code-char - (+ (* (cl-tiled:tile-row tile) 15) - (cl-tiled:tile-column tile) - 32))) - - -(defvar +qwerty-layout+ - '(#\q #\Q #\w #\W #\e #\E #\r #\R #\t #\T #\y #\Y #\u #\U #\i #\I #\o #\O #\p #\P #\[ #\{ #\] #\} - #\a #\A #\s #\S #\d #\D #\f #\F #\g #\G #\h #\H #\j #\J #\k #\K #\l #\L #\; #\: #\' #\" - #\z #\Z #\x #\X #\c #\C #\v #\V #\b #\B #\n #\N #\m #\M #\, #\< #\. #\> #\/ #\?)) - -(defvar +dvorak-layout+ - '(#\' #\" #\, #\< #\. #\> #\p #\P #\y #\Y #\f #\F #\g #\G #\c #\C #\r #\R #\l #\L #\/ #\? #\= #\+ - #\a #\A #\o #\O #\e #\E #\u #\U #\i #\I #\d #\D #\h #\H #\t #\T #\n #\N #\s #\S #\- #\_ - #\; #\: #\q #\Q #\j #\J #\k #\K #\x #\X #\b #\B #\m #\M #\w #\W #\v #\V #\z #\Z)) - - -(defun parallel-list-item (item-a list-a list-b &key (test #'eql)) - "Given two parallel lists and an item contained in the first list, return its -corresponding item in the other list, by index." - (let ((index (position item-a list-a :test test))) - (if index - (nth index list-b)))) - - -(defun normalize-char (char-plist &optional (layout +qwerty-layout+)) - "Given a character input property list (as received from read-char-plist), -massage the output into parsable, deescaped, QWERTY-according format." - (let ((normalized (deescape-char-plist char-plist))) - (setf (getf normalized :char) - (qwertyize-char (getf normalized :char) - layout)) - normalized)) - - -(defun qwertyize-char (char layout) - "Given a char input in some layout, return the corresponding character in QWERTY. -Not at all comprehensive, but probably-mostly-just-good-enough. ¯\_ (ツ)_/¯" - (or (parallel-list-item char layout +qwerty-layout+) - char)) - - -(defun deescape-char-plist (char-plist) - "Translate escaped characters into somewhat-semantically-adjacent -characters, like left arrow-key (escaped D) into ← (“LEFTWARDS ARROW”)." - (list :modifier (getf char-plist :modifier) - :char (if (getf char-plist :escaped) - (case (getf char-plist :char) - (#\A #\↑) - (#\B #\↓) - (#\C #\→) - (#\D #\←) - (otherwise (getf char-plist :char))) - (getf char-plist :char)))) - - -(defun read-char-plist (&optional (stream *standard-input*)) - "Reads a character directly from standard-input (sans buffering). -Simple terminal escape codes (like arrow-keys) are translated into -roughly-semantically-equal character representations — see the -docstring of #'escape-code-to-character for more info." - (let* ((char-1 (read-char stream)) - (char-2 (if (eq char-1 #\ESC) (read-char-no-hang stream))) ; Maybe escaped char or [. - (char-3 (if (eq char-2 #\[) (read-char-no-hang stream))) ; Maybe end-of-sequence, or 1. - (char-4 (if (eq char-3 #\1) (read-char-no-hang stream))) ; Maybe semicolon. - (char-5 (if (eq char-4 #\;) (read-char-no-hang stream))) ; Maybe modifer-key. - (char-6 (if (characterp char-5) (read-char-no-hang stream)))) ; Escaped char and EOS. - ;; Let me explain! There are pretty much three input-cases we should care about: - ;; * character - ;; * ␛ character - ;; * ␛ [ character - ;; * ␛ [ 1 ; modifier character - ;; This is by no means comprehensive, sorry: I didn't even try! But it suits my purposes. :-P - (let ((the-char (or char-6 char-3 char-2 char-1)) - (modifier (case char-5 - (#\2 'shift) - (#\3 'meta) - (#\5 'control))) - (escaped (eq char-1 #\ESC))) - (list :char the-char :modifier modifier :escaped escaped)))) +(in-package :flora-search-aurora) (defun main () @@ -188,8 +37,7 @@ docstring of #'escape-code-to-character for more info." (cl-charms:with-curses () (cl-charms:enable-raw-input :interpret-control-characters 't) (print-screen-matrix matrix) - (loop (print (deescape-char-plist (read-char-plist)))) + (loop (print (normalize-char-plist (read-char-plist)))) (sleep 5)))) - (main) diff --git a/input.lisp b/input.lisp new file mode 100644 index 0000000..2e2a741 --- /dev/null +++ b/input.lisp @@ -0,0 +1,103 @@ +;;;; Copyright © 2023, Jaidyn Ann +;;;; +;;;; This program is free software: you can redistribute it and/or +;;;; modify it under the terms of the GNU General Public License as +;;;; published by the Free Software Foundation, either version 3 of +;;;; the License, or (at your option) any later version. +;;;; +;;;; 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. See the +;;;; GNU General Public License for more details. +;;;; +;;;; You should have received a copy of the GNU General Public License +;;;; along with this program. If not, see . + +;;;; FLORA-SEARCH-AURORA.INPUT +;;;; All input-related voodoo goes here: Input reading, translating, parsing, etc. + +(defpackage :flora-search-aurora.input + (:use :cl) + (:export #:read-char-plist #:normalize-char-plist + +qwerty-layout+ +dvorak-layout+)) + +(in-package :flora-search-aurora.input) + + +;; Yup, they're hardcoded like this. Horrid, ain't it? ^_^ +(defvar +qwerty-layout+ + '(#\q #\Q #\w #\W #\e #\E #\r #\R #\t #\T #\y #\Y #\u #\U #\i #\I #\o #\O #\p #\P #\[ #\{ #\] #\} + #\a #\A #\s #\S #\d #\D #\f #\F #\g #\G #\h #\H #\j #\J #\k #\K #\l #\L #\; #\: #\' #\" + #\z #\Z #\x #\X #\c #\C #\v #\V #\b #\B #\n #\N #\m #\M #\, #\< #\. #\> #\/ #\?)) + +(defvar +dvorak-layout+ + '(#\' #\" #\, #\< #\. #\> #\p #\P #\y #\Y #\f #\F #\g #\G #\c #\C #\r #\R #\l #\L #\/ #\? #\= #\+ + #\a #\A #\o #\O #\e #\E #\u #\U #\i #\I #\d #\D #\h #\H #\t #\T #\n #\N #\s #\S #\- #\_ + #\; #\: #\q #\Q #\j #\J #\k #\K #\x #\X #\b #\B #\m #\M #\w #\W #\v #\V #\z #\Z)) + + +(defun read-char-plist (&optional (stream *standard-input*)) + "Reads a character directly from standard-input (sans buffering). +Simple terminal escape codes (like arrow-keys) are translated into +roughly-semantically-equal character representations — see the +docstring of #'escape-code-to-character for more info." + (let* ((char-1 (read-char stream)) + (char-2 (if (eq char-1 #\ESC) (read-char-no-hang stream))) ; Maybe escaped char or [. + (char-3 (if (eq char-2 #\[) (read-char-no-hang stream))) ; Maybe end-of-sequence, or 1. + (char-4 (if (eq char-3 #\1) (read-char-no-hang stream))) ; Maybe semicolon. + (char-5 (if (eq char-4 #\;) (read-char-no-hang stream))) ; Maybe modifer-key. + (char-6 (if (characterp char-5) (read-char-no-hang stream)))) ; Escaped char and EOS. + ;; Let me explain! There are pretty much three input-cases we should care about: + ;; * character + ;; * ␛ character + ;; * ␛ [ character + ;; * ␛ [ 1 ; modifier character + ;; This is by no means comprehensive, sorry: I didn't even try! But it suits my purposes. :-P + (let ((the-char (or char-6 char-3 char-2 char-1)) + (modifier (case char-5 + (#\2 'shift) + (#\3 'meta) + (#\5 'control))) + (escaped (eq char-1 #\ESC))) + (list :char the-char :modifier modifier :escaped escaped)))) + + +(defun normalize-char-plist (char-plist &optional (layout +qwerty-layout+)) + "Given a character input property list (as received from read-char-plist), +massage the output into parsable, deescaped, QWERTY-according format." + (let ((normalized (deescape-char-plist char-plist))) + (setf (getf normalized :char) + (qwertyize-char (getf normalized :char) + layout)) + normalized)) + + +(defun qwertyize-char (char layout) + "Given a char input in some layout, return the corresponding character in QWERTY. +Not at all comprehensive, but probably-mostly-just-good-enough. ¯\_ (ツ)_/¯" + (or (parallel-list-item char layout +qwerty-layout+) + char)) + + +(defun deescape-char-plist (char-plist) + "Translate escaped characters into somewhat-semantically-adjacent +characters, like left arrow-key (escaped D) into ← (“LEFTWARDS ARROW”)." + (list :modifier (getf char-plist :modifier) + :char (if (getf char-plist :escaped) + (case (getf char-plist :char) + (#\A #\↑) + (#\B #\↓) + (#\C #\→) + (#\D #\←) + (otherwise (getf char-plist :char))) + (getf char-plist :char)))) + + +;;; ~ Utilities ~ + +(defun parallel-list-item (item-a list-a list-b &key (test #'eql)) + "Given two parallel lists and an item contained in the first list, return its +corresponding item in the other list, by index." + (let ((index (position item-a list-a :test test))) + (if index + (nth index list-b))))