I thought it might be interesting to play a quick game inside AutoCAD, so yesterday i wrote a quick draft for a tetris game.
The idea was to store the game info in a 2D matrix or list of lists, to perform the backend logic, and present the user with graphics made of polyline shapes. I wrote a quick "skeleton" for the game, but when i got to the main loop and the user input, i got stuck. I can't find a way to keep the main loop running in the background, while waiting for user input with grread. After a set ammount of time, the active shape should move down, independent of any user input. I might be totally wrong with my approach, so suggestions are very welcome.
"Q" - exit
"W,S,A,D" - controls /Up,Down,Left,Right/
Everything is global, i didn't bother localizing variables at this stage.
(defun c:tetris ( / ) ;;----------------------=={ Remove Nth }==--------------------;; ;; ;; ;; Removes the item at the nth index in a supplied list ;; ;;------------------------------------------------------------;; ;; Author: Lee Mac, Copyright © 2011 - www.lee-mac.com ;; ;;------------------------------------------------------------;; ;; Arguments: ;; ;; n - index of item to remove (zero based) ;; ;; l - list from which item is to be removed ;; ;;------------------------------------------------------------;; ;; Returns: List with item at index n removed ;; ;;------------------------------------------------------------;; (defun LM:RemoveNth ( n l / i ) (setq i -1) (vl-remove-if '(lambda ( x ) (= (setq i (1+ i)) n)) l) );defun (defun CreateCanvas () (setq canvasEnt (LWPoly (list origin (mapcar '+ origin (list 0 (* rows pixelSize) 0)) (mapcar '+ origin (list (* cols pixelSize) (* rows pixelSize) 0)) (mapcar '+ origin (list (* cols pixelSize) 0 0)))) ) (ClearCanvas rows cols) );defun (defun ClearCanvas (rows cols) (setq canvas nil) (repeat rows (setq canvas (cons (repeat cols (EmptyRow)) canvas))) );defun (defun EmptyRow ( / row) (repeat cols (setq row (cons 0 row))) );defun (defun RemoveRow (rowPos / row) (setq canvas (append (LM:RemoveNth rowPos canvas) (list (EmptyRow)))) );defun (defun LWPoly (lst) (entmakex (append (list (cons 0 "LWPOLYLINE") (cons 100 "AcDbEntity") (cons 100 "AcDbPolyline") (cons 90 (length lst)) (cons 70 1)) (mapcar (function (lambda (p) (cons 10 p))) lst))) );defun (defun NewPixel (pt) (setq activeShapePos (cons (mapcar '/ pt (list 10 10)) activeShapePos)) (LWPoly (list pt (mapcar '+ pt (list 0 pixelSize 0)) (mapcar '+ pt (list pixelSize pixelSize 0)) (mapcar '+ pt (list pixelSize 0 0)))) );defun (defun Shape1 () (setq activeShapePos nil) (mapcar '(lambda (pixel) (ssadd pixel activeShape) ) (list (NewPixel spawnPoint) (NewPixel (mapcar '+ spawnPoint (list 0 pixelSize 0))) (NewPixel (mapcar '+ spawnPoint (list pixelSize pixelSize 0))) (NewPixel (mapcar '+ spawnPoint (list pixelSize 0 0))) ) ) );defun (defun SpawnShape (shape) (setq activeShape (ssadd)) (cond ((= shape 1) (Shape1)) );cond );defun ;;;(defun ShapeStop (shape) ;;; ;;;);defun ;;; ;;;(defun ShapeRotate (shape) ;;; ;;;);defun (defun MoveUp () (command "move" activeShape "" (list 0 0 0) (list 0 pixelSize 0)) );defun (defun MoveDown () (command "move" activeShape "" (list 0 0 0) (list 0 (* pixelSize -1) 0)) );defun (defun MoveLeft () (command "move" activeShape "" (list 0 0 0) (list (* pixelSize -1) 0 0)) );defun (defun MoveRight () (command "move" activeShape "" (list 0 0 0) (list pixelSize 0 0)) );defun (defun sleep (secs / time) (setq time (getvar "Millisecs")) (while (< (/ (- (getvar "Millisecs") time) 1000.0) secs) nil) );defun ;;; ;;; FUNCTION STARTS HERE ;;; (setq origin (list 0 0 0)) (setq pixelSize 10) (setq rows 20) (setq cols 10) (setq spawnPoint (mapcar '+ origin (list (* (- (/ cols 2) 1) pixelSize) (* rows pixelSize) 0))) ;;;(setq canvasX (* pixelSize cols)) ;;;(setq canvasY (* pixelSize rows)) ;;;(setq color 7) ;;;(setq activeShapeColor 3) (setq gameOver nil) (setq timer nil) (setq tick 0.5) (command "_-view" "_t") (command "_ucs" "_w") (CreateCanvas) (command "_zoom" "o" canvasEnt "") (SpawnShape 1) (while (not gameOver) (if (not timer) (setq timer (getvar "Millisecs"))) (if (< (/ (- (getvar "Millisecs") timer) 1000.0) tick) (progn (setq gRead (grread T 15 1) grCode (car gRead) grVal (cadr gRead)) (cond ;Quit game ((vl-position grVal '(113 81)) (setq gameOver T) (alert "Game Over!"));q Q ((vl-position grVal '(115 83)) (MoveDown));s S ((vl-position grVal '(97 65)) (MoveLeft));a A ((vl-position grVal '(100 68)) (MoveRight));d D ((vl-position grVal '(119 87)) (MoveUp));w W for testing );cond ) (progn (MoveDown) (setq timer (getvar "Millisecs")) ) ) );main loop (princ) );defun
Not tested, but browsing 10 year old code
(setq flg T) (while flg (redraw) (setq gr (grread T 8)) (if (< (/ (- (getvar "Millisecs") timer) 1000.0) tick) (cond ((or (equal gr '(2 81)) (equal gr '(2 113))) (setq flg nil)) ((or (equal gr '(2 83)) (equal gr '(2 115))) (MoveDown)) ((or (equal gr '(2 65)) (equal gr '(2 97))) (MoveLeft)) ((or (equal gr '(2 68)) (equal gr '(2 100))) (MoveRight)) ((or (equal gr '(2 87)) (equal gr '(2 119))) (MoveUp)) ) (progn (MoveDown) (setq timer (getvar "Millisecs"))) ) ) (alert "Game Over!")
I am not one of the robots you're looking for
Maybe i didn't state my problem clearly enough in the first post. In the main loop, the function should constantly "sense" for user input /button press/. Independent of the user input, after a given ammount of time, the function should execute the "MoveDown" code. The problem comes here. When i set up "grread" to read key presses /track = nil, bit0 = 0/, the loop halts at the grread and waits for user input indefinitely. I can't find a way to "escape" out of the grread function if no key is pressed after a while.
If track = T or bit0 = 1, any mouse movement gets captured by grread and the code can continue for another loop. That means that the user should be constantly moving the mouse, in order to keep the loop running.
If i execute a "zoom" command, just before the "grread", that is interpreted as a mouse movement and the loop keeps moving on. The problem with that "hack" is that "grread" gets flooded with data so fast, that it can't read the user's keyboard inputs anymore.
Basically that's my problem. I need to find a way to keep the main loop running and at the same time "sense" for user input. I even thought about running the loop out of the main acad process /external app in .NET/ and injecting commands into AutoCAD as needed, but that's a whole nother can of worms and defeats the purpose of writing it in lisp in the first place.
I wonder if what i am trying to achieve is even possible in lisp...
I got the loop working, but the timing is very inconsintent. I just can't get it to run in a smooth predictable pace, so i'm giving up on the idea to make such a game in AutoCAD, but i found a few useful things, while tinkering with this.
I searched the internet for a way to move a lwpolyline with entmod, but i wasn't able to find anything on the topic, so i wrote my own function.
Move a lwpolyline using entmod:
Usage:
(entmod (MassMod (entget ename) (list 0 X Y)))
(defun MassMod (lst mod / pair return) (while (setq pair (car lst) lst (cdr lst)) (setq return (cons (if (= (car pair) 10) (mapcar '+ pair mod) pair) return)) ) (reverse return) );defun
Another useful thing i found is a way to capture user input inside a loop, without halting the loop, with the help of a relatively simple .NET dll. I made a LispFunction that listens for user input, within a given timeout and if there is input, it is stored in the "keyPress" variable. If the timeout is reached, the function exits, without assigning any value to the "keyPress" variable.
This function was inspired by Kean Walmsley, so all credit goes here: https://www.keanw.com/2007/02/allowing_users_.html
Intercept user input inside a loop, withing a given timeout:
Argument: "timeout" - timeout in milliseconds /INTEGER/
The function does not return anything, but if a key is pressed, it sets the key /as a string/ in a variable named "keyPress". You can exit the function with the "ESC" key, if you set a long timeout and want to abort.
Usage:
(setq keyPress nil)
(GetInput timeout)
(cond
((= keypress "Q") (DoSomething));q Q
((= keypress "S") (DoSomething));S
((= keypress "A") (DoSomething));A
((= keypress "D") (DoSomething));D
((= keypress "W") (DoSomething));W
(T (DoSomethingElse))
);cond
using System; using System.Diagnostics; using Autodesk.AutoCAD.Runtime; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using WinForms = System.Windows.Forms; namespace ACAD_Test { public class MyCommands { [LispFunction("GetInput")] static public void GetInput(ResultBuffer resbuf) { MyMessageFilter filter = new MyMessageFilter(); WinForms.Application.AddMessageFilter(filter); try { Stopwatch sw = new Stopwatch(); sw.Start(); Document doc = Application.DocumentManager.MdiActiveDocument; Editor ed = doc.Editor; TypedValue[] args = resbuf.AsArray(); int timeout = (int)args[0].Value; while (sw.ElapsedMilliseconds < timeout) { System.Windows.Forms.Application.DoEvents(); if (filter.bCanceled == true) { ed.WriteMessage("\nGetInput function cancelled:"); break; } switch (filter.key) { case "A": doc.SetLispSymbol("keyPress", new TypedValue((int)LispDataType.Text, "A")); break; case "D": doc.SetLispSymbol("keyPress", new TypedValue((int)LispDataType.Text, "D")); break; case "S": doc.SetLispSymbol("keyPress", new TypedValue((int)LispDataType.Text, "S")); break; case "W": doc.SetLispSymbol("keyPress", new TypedValue((int)LispDataType.Text, "W")); break; case "Q": doc.SetLispSymbol("keyPress", new TypedValue((int)LispDataType.Text, "Q")); break; } } System.Windows.Forms.Application.RemoveMessageFilter(filter); } catch (System.Exception Ex) { System.Windows.Forms.Application.RemoveMessageFilter(filter); Application.ShowAlertDialog("System exception:\n" + Ex.Message + "\n" + Ex.Source + "\n" + Ex.StackTrace); } } }//Class public class MyMessageFilter : WinForms.IMessageFilter { public const int WM_KEYDOWN = 0x0100; public bool bCanceled = false; public string key = ""; public bool PreFilterMessage(ref WinForms.Message m) { if (m.Msg == WM_KEYDOWN) { // Check for the Escape keypress WinForms.Keys kc = (WinForms.Keys)(int)m.WParam & WinForms.Keys.KeyCode; if (kc == WinForms.Keys.Escape) { bCanceled = true; } key = kc.ToString(); // Return true to filter all keypresses return true; } key = ""; // Return false to let other messages through return false; }//Method }//Class }//Namespace
I Hope you find these functions useful, so that at least i won't have wasted my time in vain.
I found a lot of things i've been doing wrong. I'm sure my approach is wrong, but i stuck to it. Anyway, as a proof of concept at least...
Rotation is not implemented.
https://streamable.com/22947
Looks good! This is already with the keypress DLL? I would propose to use blocks (drawn with a byblock color) for the pieces - rotation and fancy designs will be easier.
Vladimir Michl, www.cadforum.cz - www.cadstudio.cz
Not a Tetris, but also based on the KeyboardInput - Tennis game for AutoCAD:
via https://www.cadstudio.cz/en/the-17th-christmas-cad-freeware-giveaway-by-cad-studio-art3562
Vladimir Michl, www.cadforum.cz
The program can not wait for a flag to be set by a grread. It's got to work something something like this pseudo code:
(set done nil) ;; or localize it (while (not done)
(setq code nil) (while (> timeleft 0) (if (> timeleft 0))
(setq code (grread etc))
(decrement timeleft)
) (cond
((= timeleft 0)(move_down)) ((= code this)(do_this)) ((= code that)(do_that)) ((equal code <quit>)(setq done 1)) ) ) (move_down) )
;; I guess the timeleft should be reset within move_down.
;; I dunno. I should be eating rather than drinking.
But I think even that requires at least moving your pointer, even if only an angstrom.
John F. Uhden
@john.uhden
No matter how you structure the code, the problem remains the same. Once the program reaches the "grread" function, it will stop execution until there is some type of user input. To get around that, you either need a background thread, constantly monitoring for user input and executing functions accordingly, or set a timeout on your user input function, to break out of it once that timeout window has passed. I wanted to control the main loop from lisp, so i opted to go for a grread-like function with a built in timeout, written in .NET.
John F. Uhden
Can't find what you're looking for? Ask the community or share your knowledge.