The Othello demo is a simple Android app showing the basics of using the LispWorks for Android Runtime. It is a full Android project that can be imported into Android Studio.
The application plays the Othello game as an example of an application. When delivering "with Lisp" (see 16.4.2 Delivering LispWorks to the project below), it also allows the user to type and evaluate Lisp forms. This is useful during development.
The example also demonstrates how to create two separate APK files, one for ARM 64-bit machines and one for ARM 32-bit. This is useful to reduce the size of the APK that users need to load. For playing with the demonstration, you need only one of the architectures, so can skip steps that are specific to one of the architectures. See 16.1.2 ABI splitting using flavors in the OthelloDemo for a discussion of this mechanism in the demonstration.
There following file contains information about the example:
(example-edit-file "android/README.txt")
To try the demo, you need to do these steps:
These steps are described in detail in the following sections.
The Android project code is in the OthelloDemo
directory, which is the directory examples/android/OthelloDemo
inside the LispWorks distribution. You need to make a project with this code.
First, copy the contents of OthelloDemo
directory recursively to some other directory where Android Studio can write (this is needed because the examples directory is supposed to be read-only). The new copy is referred to below as the "project directory".
In Android Studio, select File -> New -> Import Project..., or the "Import project" item in the "Welcome to Android Studio" dialog, (in Android Studio 3.3.1, the exact text of the item is "Import project (Gradle, Eclipse ADT, etc.)"). This raises a dialog asking for the project to import. Enter the full path of the project directory that you copied above. Once you have imported the project, it can be built and run, but it does not have the Lisp parts yet, so the application just gives an error on start up that it fails to find the library. You will need to deliver the Lisp part as described in 16.4.2 Delivering LispWorks to the project below before the application will work.
To deliver LispWorks, copy one of the build script files deliver-android-othello.lisp
or deliver-android-othello-with-lisp.lisp
from the examples/android
directory in the LispWorks distribution. In the copied file, change the value of the variable *project-path*
to point to your project directory, that is the copy of OthelloDemo
from the previous section. For example:
(defvar *project-path* "~/my-workspace/LispWorksRuntimeDemo/")
You will then use your edited copy of the build script as the -build
command line argument to LispWorks.
The Android delivery images are called lispworks-8-0-0-arm-linux-android
, lispworks-8-0-0-arm64-linux-android
, lispworks-8-0-0-x86-linux-android
and lispworks-8-0-0-amd64-linux-android
, which must be run on an appropriate architecture on Linux or macOS. The images can be run using an emulator such as QEMU if necessary using the script examples/android/run-lw-android.sh
as follows:
run-lw-android.sh -build <path-to-a-modified-copy-of>/deliver-android-othello.lisp
If you run this on an x86 Linux machine it will also build images for the 32-bit and 64-bit x86 architectures. To run these with the x86 Android Emulator, you will also need to uncomment the x86 flavors in the Gradle file app/build.gradle
inside the Android project.
See deliver-to-android-project for details of the delivery process. Note that the script run-lw-android.sh
tries to deliver all four combinations of 64-bit/32-bit and x86/ARM . This creates two files for each architecture (relative to the project directory):
ARM 32-bit |
|
ARM 64-Bit |
|
x86 32-bit |
|
x86 64-Bit |
|
If you cannot access the project directory from the Linux or macOS machine:
armeabiv7a
, arm64v8a
, x86
and x86_64
. These specific names are recognized by deliver-to-android-project.*project-path*
in the build script to the "deliv directory" and deliver using run-lw-android.sh
as above. deliver-to-android-project will recognize the sub-directories and write the files into them.sourceSets
block in the app/build.gradle
file in the Android Studio project directory to map the source sets names arm64v8a
, armeabiv7a
, x86
and x86_64
to the sub-directories in the "deliv directory". The example already contains such a block which is commented out. Uncomment it and edit it as needed.
If you cannot access the "deliv directory" from the the machine on which you run Android Studio, then recursively copy the contents of the sub-directory arm64v8a
from the "deliv directory" to the app/src/arm64v8a
sub-directory inside the Android Studio project directory, and similarly for the sub-directories armeabiv7a
, x86
and x86_64
, so you will end up with the same files that are listed above for the four architectures.
Once you have the project with the LispWorks files, you can build, install it on the device and run it as any other Android project. When it runs, It first shows a splash screen (the LispWorks splash screen image) and then the first screen displays an Othello board, where you can play against the computer (you play black), by touching the square where you want to add your piece.
The display has two elements in addition to the board:
It also has a menu (which maybe partly displayed on the action bar), with these items:
Restart |
Restart the game. |
Undo | Undo the last move. You can undo repeatedly to the beginning of the game. When the computer plays, each undo undoes to the state before your last move. When the computer does not play, it undoes one move. |
When delivering "with Lisp" the menu also has these items:
Lisp Panel |
Takes you to the Lisp Panel screen, which allows you to evaluate Lisp forms. See below in the description of the Lisp Panel. |
Command history | Takes you a list of the forms that that you evaluated. It is initialized by a few demo forms. See below about the History list. |
Othello Server |
Raises a submenu with three items: Java server, full proxy and lazy proxy. Switching between these changes the mechanism by which Java calls into Lisp. The behavior of the game is exactly the same, only the output to the Lisp Panel or Output is different. This feature is for demonstrating different techniques of calling from Java to Lisp. See discussion of the code for details. |
When delivering "without Lisp" the menu also has these items:
Output |
Takes you to the "output" screen. |
The Lisp Panel contains a row of buttons, a text view for input, and the bottom is a text view for output. This screen is available only when delivering "with Lisp". When delivering "without Lisp", there is the Output screen instead.
The buttons are:
Clear |
Clears all the output from the output pane. |
Evaluate the string |
Send the current text in the input pane to Lisp by a direct call to
|
History | Takes you to another screen which displays a list of the forms that were evaluated. The list is initialized by some forms which demonstrate some features of the multiprocessing on Android. See below in the section 16.4.3.2 Prepared forms. Whenever you evaluate a form by pressing Evaluate the string, it adds the form to the history in the beginning of it. If the form matches exactly a form which is already in the list, the old item is removed. In the history list, when you touch an item it is inserted into the input pane, and the application switches to the Lisp Panel. It does not evaluate the form at that point. You can also reach the history list from the menu in the Othello screen. |
Bug form logs |
Invokes com.lispworks.Manager.showBugFormLogs. This shows another screen with a list of the logged errors displaying the error string for each item. Touching an item opens another screen with bug form log of this error. |
Clear logs |
Clears all the bug form logs, including removing the files. |
The input pane below the buttons is just a passive text view, in which you can type Lisp forms, and evaluate by touching the Evaluate the string button.
The bottom part of the Lisp Panel, in the Output screen when delivering "without Lisp", is the output pane. It prints the output of evaluation as above. It also prints whenever you touch a square in the Othello board. When the Full or Lazy proxy is used for communication, it also prints this fact.
Initially, the History list contains the forms described below. When using forms, note that evaluating a form moves it to the top of the list. When you should evaluate more than one of these forms in order, you will need to look down the list for each one in turn.
The idea is that you can try these forms, and then modify them to check and perform things that you need to do when debugging your application.
Forms:
(mp:ps)
Shows the Lisp processes. Initially there are at least the idle process and the GUI process which displays as "created by foreign code".
(setq *computer-plays-waste-time-in-seconds* 2)
That causes the computer to pretend that it takes it time to compute a move. When playing against the computer after setting this, you will see that after your move, the display says "Computer to play" for two seconds before it actually plays. Set *computer-plays-waste-time-in-seconds*
back to nil
to make it behave normally.
(defun eval-and-print (form) (let ((res (eval form))) (lw-ji:send-message-to-java-host (princ-to-string res) :reset)))
Defines a function to be used by the next two forms. Note that it uses send-message-to-java-host to print, which comes in the output and works on any thread. When it is on the current thread it will end up printing before the printing of the evaluation, but on another thread it is random which output comes first.
(eval-and-print '(mp:get-current-process))
Use the function defined above to print the process in the current thread. That is the GUI process.
(mp:funcall-async 'eval-and-print '(mp:get-current-process))
Use the function eval-and-print
defined above to print the process on which funcall-async executes the function. This will be one of the Background Execute processes.
(progn (defun loop-executing-events () (loop (let ((event (mp:process-wait-for-event))) (lw-ji:format-to-java-host "~%got event ~s" event) (let ((res (mp:general-handle-event event))) (lw-ji:format-to-java-host "~%Handling got ~s" res))))) (setq loop-executing-events-process (mp:process-run-function "Loop Execute Events" () 'loop-executing-events)))
Create a process called "Loop Execute Events" and set loop-executing-events-process
to it. The process has a process function loop-executing-events
which read events and handles them using process-wait-for-event and format-to-java-host. It prints "got event <event>" and then "handling got <result of handling>". Note the usage of format-to-java-host, which prints to the output pane too (it actually calls send-message-to-java-host).
(mp:process-send loop-executing-events-process '(mp:get-current-process))
Sends to the "Loop Execute Events" process (that started in the previous step) an event, which cause get-current-process to be called, and hence return the process. You should see "got event (MP:GET-CURRENT-PROCESS)" and "Handling got <process name>".
(othello-user-change-a-square 5 2)
Changes square 5 (sixth from the left in the top row) to color 2 (black). This function is defined in (example-edit-file "android/android-othello-user")
and is part of the "interface" that the Lisp Othello code uses to tell Java to change the board.
(mp:process-run-function "multiplier" () #'(lambda() (setq *finish-multiply* nil) (dotimes (x 100) (sleep 1) (when *finish-multiply* (return)) (lw-ji:format-to-java-host \"~%~d * ~d = ~d\" x x (* x x)))))
Starts a process that performs "a lengthy computation" (simulated by using (sleep 1)
) and prints results while doing it. In each "step in the computation" (the cl:dotimes iteration) it prints the square of the iteration number. To stop it, evaluate the next form.
(setq *finish-multiply* t)
Tell the "multiplier" process (see above) to stop.
(mp:process-run-function "Error" () #'(lambda () (open "junk;;file::name")))
Starts another process that gets an error (because the argument to cl:open is an illegal pathname). It prints that it got the error, and you can use the Bug form logs button to look at the bug form log.
(raise-alert-dialog "What do you want to eat?" + :ok-title "Chicken " :ok-callback '(raise-alert-dialog "Here is some chicken") + :cancel-title "Salad " :cancel-callback '(raise-alert-dialog "We do not have salad"))
Raises an alert dialog using raise-alert-dialog
which is defined in dialog.lisp
. Note that this works because the LispPanel
class uses com.lispworks.Manager.setCurrentActivity to set the current activity.
(raise-a-toast "Bla Bla Bla" :gravity :left)
Raises an Android "toast" at the middle of the left side, using raise-a-toast
which is defined in toast.lisp
.
The Othello Demo Java code is in the package com.lispworks.example.othellodemo
. LispWorks interfaces in Java are all in the package com.lispworks
. The methods appear in full, to make it is easy to see where there is a call to the LispWorks interface.
Othello
is a subclass of Activity
that displays the screen with the Othello board. The display is all in standard Java. The board is made of a grid of 64 ImageView
panes, each one displaying one of three images (blank, white, black). Each view has an OnClickListener(SquareListener)
that remembers its index and passes it when clicked.
The Java code does not know anything about the game that is being played, and does not keep a record of the state of the game. That is all done in Lisp.
The Java code processes user gestures concerning the game (touching the board, and touching any of the buttons and items Computer plays, undo move, restart) by calling methods on an object that implements the nested interface OthelloServer
, which is kept in mOthelloServer
. The object can be either a Lisp proxy, or of the nested class JavaOthelloServer
. All of these objects do exactly the same thing (calling the Lisp functions defined in (example-edit-file "misc/othello")
), and the purpose of having all these options is to demonstrate different techniques to call into Lisp. There is also a nested class ErrorOthelloServer
in case LispWorks does not work, which displays the error. mOthelloServer
is set by the method setupServer
.
The nested class JavaOthelloServer
is plain Java with methods that call into Lisp using the 15.3.1 Direct calls interface (com.lispworks.LispCalls.callIntV and com.lispworks.LispCalls.callVoidV). This has the advantage that on the Lisp side all you have to do is to ensure that the functions are not shaken, which you can do with hcl:deliver-keep-symbols (see the Delivery User Guide). It has the disadvantage that you hardwire Lisp function names in Java (though the names can be variables too).
The other two possible implementations of the OthelloServer
are Lisp proxies which are defined in Lisp (in examples/android/android-othello-user.lisp
). See the discussion of the Lisp code for more details. The code in setupServer
demonstrates two techniques of using the proxy definitions: either calling a Lisp function that makes a proxy (using com.lispworks.LispCalls.callObjectV to call create-lisp-othello-server
), or using com.lispworks.LispCalls.createLispProxy with the name of the proxy definition (lisp-othello-server-lazy
) to create a proxy.
To actually respond to moves, the Othello
class exports 3 methods ("updateState"
, "signalBadMove"
and "change"
) which are called directly from Lisp to change the board and the status text.
When an Othello
instance is created, it calls setupAndInit
to do anything with Lisp (mainly call mOthelloServer.init
). Before doing anything that may interact with Lisp, it checks the status of Lisp using com.lispworks.Manager.status. If Lisp is not ready and there was no error, it calls com.lispworks.Manager.init to initialize LispWorks, passing it a Runnable
that with call setupAndInit
again to actually do the initialization. In the Demo the Lisp side will already be initialized, because it is done by the LispWorksRuntimeDemo
activity, but the Othello
class avoids relying on it.
When LispWorks is ready, setupAndInit
sets up the server by calling setupServer
and initializes the game by calling mOthelloServer.init
.
If there is an error, setupAndInit
gets the error details using com.lispworks.Manager.mInitErrorString, and com.lispworks.Manager.init_result_code and adds a message, set mOthelloServer
to ErrorOthelloServer
, and then shows the Lisp Panel which will be displaying the error.
There is also an onCreateOptionsMenu
method which checks whether Lisp is working and can evaluate forms (using LispPanel.canEvaluate
), and accordingly decides which menu to use.
LispPanel
is a subclass of Activity
that displays the Lisp panel, or just the output when delivering "without Lisp" (see 16.4.2 Delivering LispWorks to the project).
The main purpose of the Lisp Panel is to evaluate Lisp forms, which it does by calling the Lisp function eval-for-android
using com.lispworks.LispCalls.callIntV. That can work only if eval-for-android
is defined, so LispPanel has a method canEvaluate
that works by checking if eval-for-android
is defined using com.lispworks.LispCalls.checkLispSymbol. If eval-for-android
is fbound, LispPanel
displays in full, otherwise it shows only output TextView
.
LispPanel
is also responsible for displaying messages in its output TextView
. To achieve that, it uses com.lispworks.Manager.setTextView. Once it sets the TextView
, all calls to com.lispworks.Manager.addMessage and calls to the Lisp functions send-message-to-java-host and format-to-java-host put their output in this TextView
.
Other usage of the com.lispworks
package in LispPanel
are:
onResume
and onPause
to allow Lisp code to raise dialogs when LispPanel
is visible. This is needed to allow the raise-alert-dialog
form to work.
MyApplication
is not actually used in the demo. It is a demonstration of how to initialize LispWorks when the application starts, by calling com.lispworks.Manager.init in the onCreate
of the application. The demo itself does not use this mechanism. Instead the SplashScreen
activity does it, and the Othello
activity also checks using com.lispworks.Manager.status, and if LispWorks needs initializing does it.
Display a splash screen and initialize the Lisp side, by checking com.lispworks.Manager.status and using com.lispworks.Manager.init if needed. The purpose of this class is just to give an example of displaying a splash screen while initializing Lisp. It is not really needed, because the Othello
class checks too (in setupAndInit
). On Eclipse the name of this class is the default project name.
A simple class to display Lisp forms. Does not do anything related to Lisp.
A simple class to make a square layout for displaying the Othello
board. Does not do anything related to Lisp.
The file:
(example-edit-file "misc/othello")
is a generic implementation of the playing Othello part, and has nothing to do with Java or Android.
The Lisp code that interacts with Java and Android to play Othello and evaluate the forms is in:
(example-edit-file "android/android-othello-user")
The Java callers to update the game are defined by a define-java-caller form. All these methods need to be called on the GUI thread (because they interact with GUI elements), so the actual functions that are called from the Othello code are defined to call the Java callers using android-funcall-in-main-thread.
The function eval-for-android
is what the Java code uses to evaluate Lisp forms. The function has no Java-specific features, but it has error handling and binding of some of the top-level variables like cl:* to make it more usable in repeated calls from "outside".
The code also defines two proxy definitions that implement the Othello.OthelloServer
interface which responds to user gestures. To demonstrate the various features of proxies, there are two definitions which achieve exactly the same thing. The full proxy definition (lisp-othello-server-full
) specifies functions for all the methods that the interface defines. The lazy (programmer) proxy definition does not define any method. Instead it has a default function that decides what to do based on the method name.
Note that the Othello logic can also be run via a desktop application using:
(example-edit-file "capi/applications/simple-othello")
The two files:
(example-edit-file "android/dialog")
and:
(example-edit-file "android/toast")
define the functions raise-alert-dialog
and raise-a-toast
respectively, to demonstrate using Android code directly from Lisp. See the comments in these files.
LispWorks® User Guide and Reference Manual - 01 Dec 2021 19:30:21