A Simple Lunar Lander Game in COAST

Now that we have a basic understanding of how COAST works, we can explore a more complex COAST example in the form of a simple Lunar Lander game.

Game Overview

The Lunar Lander is derived from a computer game that first appeared in the 1960's. The gameplay is simple. The player (pilot) controls the descent rate of an Apollo-era lunar lander by adjusting the lander's throttle to control the descent engine. The throttle controls the fuel burn rate, thus adjusting the velocity of the lander. The pilot must land the vehicle with a limited about of fuel. The object of the game is to land with a descent rate of less than 5 fps.

Lunar Lander Architecture

The Lunar Lander game is composed of two islands, a simulation server and a pilot shown in the architecture diagram below. The simulation server island contains a spawn service islet that listens for message that contain Motile code. Upon receiving such a message it spawns a new worker islet to execute the given Motile code within a predefined binding environment. These messages containing a simulation are passed using CURL, C1. Any island wishing to spawn a simulation on this server must present this CURL. In our simple example, the simulation server serves a purely pedagogical purpose as the server does not provide any particular services related to a lunar lander simulation. It simply executes the given Motile code within its environment. However we can imagine that such a simulation server could provide a number of specialize simulation services such as complex calculations that more accurately model gravitational pull or a selection of terrain maps for different celestial bodies.

In this example, a pilot island defines a new simulation in Motile and sends it in a message to the simulation server. The executing simulation continually recalculates the state of the lander and communicates this information back to the pilot. The state information is comprised of the current altitude, fuel level, and velocity of the lander. The pilot island displays this state information to the player who can respond by adjusting the lunar lander throttle. The pilot island creates an islet to handle user input, in this case, a keyboard response to adjust the throttle.

State communications are sent to the pilot on a predetermined CURL, C4, that is hard coded into the Motile code. The CURL for communicating throttle updates from the pilot to the simulation must be created dynamically after the islet executing the simulation has been spawned. This CURL, C3, is created at the start of the simulation execution and communicated back to he pilot via a promise, a one time communication duplet, with the CURL C2, also hard coded into the Motile code.

Lunar Lander architecture

Execution and Gameplay

In order to run the Lunar Lander, open both the simulation server (sim-server.rkt) and the pilot (pilot.rkt) in DrRacket. Load each into memory by executing the "Run" command.

First execute the simulation server island by typing this command into the sim-server.rkt console:

(island/start sim-server)

Now execute the pilot island by typing this command into the sim-server.rkt console:

(island/start pilot)

Once all communication paths have been established, your screen should look something like this:

Lunar Lander start play

As the pilot adjusts the throttle we can see messages passing back and forth between the simulation and the pilot:

Lunar Lander play

An example of a winning game:

Lunar Lander win

An example of a losing game:

Lunar Lander lose

The Simulation Server Island

Simulation Server Start-up

The declaration of our simulation server island is shown below. Here we declare a new island using the example/island/new function. This utility function (see Utility Code for Islands/Keystore) creates a new island and sets its keystore. The island start up code is set to sim-sever/boot This code will run when

(island/start sim-server)
is executed.


(define sim-server
  (example/island/new 'sim-server "sim_server_secret" sim-server/boot))
    

The island start up code is defined by the sim-server/boot function. This code defines then executes the function sim/spawn which creates a new islet that provides a simulation spawn service defined by service/spawn/simulation.


(define (sim-server/boot)
  (display "Running simulation server's boot function.\n")
  
  ; Create an islet that will receive spawn requests to run simulations.
  (define (sim/spawn)
    (let ([x  ; Create a new islet.
           (islet/new      
            (this/island)
            'server.simulation
            TRUST/MODERATE
            environ/null environ/null)])
      (islet/jumpstart
       x
       service/spawn/simulation))) ; Executes service/spawn/simulation in the new islet.

  (sim/spawn)
  (display "simulator: boot completed.\n"))
    

Simulation Spawn Service

The service/spawn/simulation function is executed on the 'server.simulations islet, the simulation server's spawn service. The code for the islet's spawn service is shown below.

On line 4, the spawn service creates a new duplet on which it can receive spawn requests. Notice the use of islet/CURL/known/new. We use this function because we will be communicating on a known (pre-determined and agreed upon) CURL whose service path is '(service spawn) and whose access id is 'ingress.service.spawn. This is CURL C1 in our architecture diagram above.

At line 8, the spawn service enters a loop where on each iteration it waits to receive a message on the duplet's receiver. Note that it uses duplet/block as its receiver which will wait until a message is received. In this case, the message will contain Motile code for the simulation to be executed. After a simulation is received a new subislet (an islet within an islet), called worker, is defined on line 11. The worker is spawned on 13 passing the Motile code contained in the received message for execution.


(define (service/spawn/simulation) ; A service to spawn a simulation
  (display "simulator: starting spawning service.\n")
  (let ([d  ; Create a duplet (a CURL/egress-point pair) for spawn requests.
         (islet/CURL/known/new
          '(service spawn) 'ingress.service.spawn GATE/ALWAYS environ/null)])
      
    (let loop ([m (duplet/block d)]) ; Wait for a spawn request.
      (let ([payload (murmur/payload m)]) ; Extract the murmur's payload.
        (when (procedure? payload) ; Check if the payload is a procedure.
          (let ([worker (subislet/new 'simulation TRUST/LOWEST SIMULATOR/ENVIRON)])
            (display "simulator: spawning simulation ...\n")
            (spawn worker payload 900.0)))) ; Limit lifespan of simulation to 900 seconds.
            (display "simulator: simulation spawned\n")
      (loop (duplet/block d)))))
    

See the entire source code for the sumilation server below (or view it in Github):


#lang racket

(require
  "../../include/base.rkt"
  "../../baseline.rkt"
  "./sim-server-env.rkt"
  [only-in "../../CURL/base.rkt" CURL/origin CURL/path CURL/metadata CURL/access]
  "../../promise.rkt"
  "../../remote.rkt"
  "../../transport/gate.rkt"
  "./coastapp-utils.rkt")

(provide sim-server)

(define (service/spawn/simulation) ; A service to spawn a simulation
  (display "simulator: starting spawning service.\n")
  (let ([d  ; Create a duplet (a CURL/egress-point pair) for spawn requests.
         (islet/CURL/known/new
          '(service spawn) 'ingress.service.spawn GATE/ALWAYS environ/null)])
      
    (let loop ([m (duplet/block d)]) ; Wait for a spawn request.
      (let ([payload (murmur/payload m)]) ; Extract the murmur's payload.
        (when (procedure? payload) ; Check if the payload is a procedure.
          (let ([worker (subislet/new 'simulation TRUST/LOWEST SIMULATOR/ENVIRON)])
            (display "simulator: spawning simulation ...\n")
            (spawn worker payload 900.0)))) ; Limit lifespan of simulation to 900 seconds.
            (display "simulator: simulation spawned\n")
      (loop (duplet/block d)))))

(define (sim-server/boot)
  (display "Running simulation server's boot function.\n")
  
  ; Create an islet that will receive spawn requests to run simulations.
  (define (sim/spawn)
    (let ([x  ; Create a new islet.
           (islet/new      
            (this/island)
            'server.simulation
            TRUST/MODERATE
            environ/null environ/null)])
      (islet/jumpstart
       x
       service/spawn/simulation))) ; Executes service/spawn/simulation in the new islet.

  (sim/spawn)
  (display "simulator: boot completed.\n"))

;fire up the sim-server island
(define sim-server
  (example/island/new 'sim-server "sim_server_secret" sim-server/boot))

(island/log/level/set 'warning)
        

The Pilot Island

Curl for Simulation Service

The simulation service has explicitly granted our pilot permission to spawn simulations by providing it a textual representation of a communication CURL which is embedded into the pilot code as follows. Hard-coding a CURL in our code is analogous to hard-coding a URL to connect to a service.


;; CURL pre-generated by the simulation server, this grants the pilot the authority to spawn simulations
(define/CURL/inline SIM-SERVER/CURL/SPAWN
  #<<!!
SIGNATURE = #"zVMpXbsnYES0IRooE3QI4a1FpwKws-CilmEexuawFCeCKXcBpZhBHzz9vzXYqpv6nf6hYJ6_qvInhnX2YrpECg"
CURL
    id = dda12627-9e96-4efe-8d8d-8dc8896cebf6
    origin = #"RaQDnsBmoxoaCe_rkNuPJB1Q7PgSaYm17jzafmYFPSc"
    path = (service spawn)
    access/id = ingress.service.spawn
    created = "2016-06-22T16:25:22Z"
    metadata = #f

!!
  )
    

The pilot must use the textual representation to construct an in-memory CURL instance using the CURL/zpl/safe-to-CURL function. The resulting CURL is C1 in our architecture diagram above. It is passed to the pilot boot function at startup where it will be used to send the compiled Motile code (thunk) to the simulation server. Note that the Utility Code for Islands/Keystore creates the global keystore, called KEYSTORE, used when constructing this CURL.


; Construct an in-memory CURL instance of the predefined CURL for sim-server.
(define sim-server/CURL/spawn (CURL/zpl/safe-to-CURL SIM-SERVER/CURL/SPAWN KEYSTORE))
    

Pilot Start-up

The declaration of our pilot island is shown below. Here we declare a new island using the example/island/new function defined in the Utility Code for Islands/Keystore). The island start up code is set to pilot/boot, passing it our CURL for spawning simulations, C1. This code will run when

(island/start pilot)
is executed.


; fire up the pilot island
(define pilot
  (example/island/new
   'pilot          ; Name of pilot island.
   "pilot_secret"  ; Filename of island certificate.
   (lambda ()      ; Bootstrap thunk for island.
     (pilot/boot sim-server/curl/spawn))))
    

The island start up code is defined by the pilot/boot function. This function is passed in the simulation spawn CURL as server/u. On lines 5-7, the pilot checks to make sure the issuer of this CURL, the simulation server, is running. It waits until it can detect it before proceeding.

On lines 14 through 17 the pilot island creates 2 CURLs that the simulation will uses to send messages to the pilot. Line 14 defines a duplet that will be used to receive state information updates from the running simulation. Line 15 defines a promise duplet that will be used to receive a one-time message from the simulation containing a CURL that the pilot will use to send thruster updates to the running simulation. This passed CURL is C3 in our architecture diagram. On line 16 the state CURL is extracted from the duplet as state@ which is C4 in our diagram, On line 17 the one-time CURL is extracted from the promise duplet as boot@ which is C2 in our diagram. Both the state@ and boot@ CURLs are compiled into our simulation.

Lines 18-20 compile the Motile code for our simulation and create a thunk, passing in arguments for the binding environment and our state@ and boot@ CURLs. The call to island/compile transpiles the Motile closure for our simulation into a native closure of the Racket/Scheme language generating a function that will return a thunk, (a zero-argument closure). The call to motile/call on line 20 invokes that function passing to the thunk the binding environment and any necessary arguments for the execution of the thunk. In this case, state@ and boot@ CURLs. On line 23 we send the resulting thunk to the simulation server's spawning service.

Next, on lines 28 and 29 we wait for the running simulation to communicate CURL C3 back to us using the boot@ CURL, C2. Once we have the C3 CURL, saved as thruster@ we can use it to send the pilot's thruster updates back to the simulation. On line 31 we make a call to create a new islet running a keyboard input reading service, passing it thruster@. The service will listen for thruster updates from the keyboard and send thruster update requests to the simulator.

Finally, on lines 34-37 we enter a loop in which we repeatedly listen for state updates from the running simulation and display the new state information to the pilot.


(define (pilot/boot server/u)
  (display "pilot: booting ...\n")
  
  (display "pilot: waiting for simulator ...\n")
  (island/enter/wait (curl/origin server/u))
  (display "pilot: saw simulator\n")
  
  ; state/d: duplet (CURL/egress pair) on which pilot will receive state updates.
  ; thruster/p: promise to be resolved by simulation. Resolution will be a CURL on
  ; which pilot will send thruster values.
  ; state@: CURL on which simulator will transmit state updates.
  ; 
  (let* ([state/d (islet/curl/new '(state notify) GATE/ALWAYS #f INTER)]
         [thruster/p (promise/new)]
         [state@ (duplet/resolver state/d)]
         [boot@  (promise/resolver thruster/p)]
         [factory (island/compile SIMULATION-FACTORY)]
         [THUNK/SIMULATION
          (motile/call factory BASELINE state@ boot@)])
    
    (display "pilot: sending simulation thunk to simulator ...\n")
    (send server/u THUNK/SIMULATION)
    (display "pilot: simulation thunk sent\n") 
    
    ; Listen for curl on which to send thruster updates to simulation.
    (display "pilot: waiting for thruster curl from simulation\n")
    (let* ([m (promise/block thruster/p)]  ; Listen for the CURL.
           [thruster@ (murmur/payload m)]) ; Extract CURL for thruster updates.
      (display "pilot: received thruster CURL from simulation\n")
      (pilot/input thruster@) ; Start dedicated islet for reading pilot input.
      
      ; Listen for lunar lander state events.
      (let loop ([m (duplet/block state/d)])
        (let ([state+ (murmur/payload m)]) ; Extract the murmur's payload.
          (display state+)(newline))
        (loop (duplet/block state/d))))))
    

Pilot Input Reader

This code creates a new islet running a keyboard input reading service for the pilot. The service will listen for thruster updates from the keyboard and send thruster update requests to the simulator.

The the new islet is created in the pilot/input function. It is passed the thruster/read function which the islet will execute along with the thruster@ CURL, C3. The thruster/read enters a loop in which it repeatedly calls the racket read function to get keyboard input from the pilot. The input is simply a numeric value to which the throttle should be reset. This value is passed to the running simulation on the thruster@ CURL.

    
;; Input reader service for the pilot.
(define (thruster/read thruster@)
  (display "pilot: Running input reader for thruster values\n")
  (let loop ([value (read)]) ; Prompt pilot for thruster update.
    ; Send thrust value to executing simulation.
    (send thruster@ (cons 'THRUSTER value))
    (loop (read))))

; Create a nanoservice dedicated to reading pilot input
; for the thruster.
(define (pilot/input thruster@)
  ; First create a new islet.
  (let ([x (islet/new
            (this/island)
            'pilot.input
            TRUST/MODERATE
            environ/null environ/null)])
    (islet/jumpstart x (lambda () (thruster/read thruster@)))))
    

Motile for Simulation

Below is the code for the simulation which will be passed to the simulation server for execution. This code is written in Motile and must first be passed through a Motile compiler in order to produce a thunk (a zero-argument closure). The Motile compiler evaluates the expression (begin e1 ··· em ) in the context of execution site〈E,b〉and returns the value of em, in this case the higher order function, simulation/new, that returns a lunar lander simulator as a thunk.

Lines 6-41 define a number of utility functions and values to support the simulation. They are somewhat self-explanatory. We will focus instead of communication and coordination between the pilot and the executing simulation.

The main function which starts the simulation is simulation/new at line 103. On line 108 we create a new duplet, thruster/d on whose CURL the pilot will send thruster updates to the executing simulation. This CURL is C3 in our architecture diagram. One line 110 we extract the CURL from this duplet using duplet/resolver and send it to the pilot using the boot@ CURL, C2, which was passed as an argument into our thunk when it was created.

On line 116 we enter into our simulation loop which starts on line 52. At each iteration we check for a thruster message from the pilot using duplet/try on our thruster duplet which will receive any messages on the C3 CURL. This is a non-blocking check. Either it will find a waiting message or it will not.

The resulting message is passed into the loop along with the current state of the lunar lander (altitude, fuel, velocity) and the current thruster setting. If no thruster update message is waiting, (line 56), the simulation recalculates the current state of the lander and sends the updated state to the pilot on CURL C4. It then checks the altitude to see if a touchdown has occurred (altitude is 0) and, if so, notifies pilot of either a successful or crash landing. Otherwise it re-enters the simulation loop with the updated state.

If, instead, there is a waiting thruster update (line 76), the simulation loop is re-entered with the updated thruster value. In the case that the fuel has been depleted that thruster value is instead set to 0 and will remain at the value, regardless of any pilot updates, for the remainder of the game.

   
(define SIMULATION-FACTORY
  ; Motile source follows.
  '(begin
    ; Access routines for lunar lander state tuple: .
    (define (altitude/get v) (tuple/ref v 0))
    (define (fuel/get v)     (tuple/ref v 1))
    (define (velocity/get v) (tuple/ref v 2))
    
    (define (fuel/empty? state) (= (fuel/get state) 0))
    (define (touchdown? state) (= (altitude/get state) 0))
    (define (landing/soft? state)
      (<= (velocity/get state) MAX_LANDING_VELOCITY))
    (define (command/thruster? command)
      (and
        (pair? command)
        (eq? (car command) 'THRUSTER)
        (integer? (cdr command))
        (>= (cdr command) 0)))
     
    (define (state/summary state)
      (format "Altitude: ~a Fuel: ~a Velocity: ~a" 
              (round (altitude/get state))
              (round (fuel/get state))
              (round (velocity/get state))))

    ; Initial lunar lander state #(altitude fuel velocity).
    (define (INITIAL-STATE) (tuple 1000.0 750.0 70.0))
    
    (define GRAVITY 1.4)  ; Gravitational constant.
    (define MAX_LANDING_VELOCITY 5) ; Upper bound for a soft landing.
    
    ;; Given a state tuple and a thruster value
    ;; (the number of units of fuel to burn)
    ;; calculate and return a successor state tuple.
    (define (state/recalculate state thruster)
      (let
          ((altitude+ (max 0 (- (altitude/get state) (velocity/get state)))) ; New altitude.
           (fuel+ (max 0 (- (fuel/get state) thruster))) ; Remaining fuel.
           (velocity+  ; ((velocity + GRAVITY)*10 - (thruster*2)) / 10
            (max 0 (/ (- (* (+ (velocity/get state) GRAVITY) 10) (* thruster 2)) 10))))
        
        ; Check if we just ran out of fuel.
        (when (and (= fuel+ 0) (> (fuel/get state) 0))
          (display "Fuel depleted\n")      ; Issue a notice on the simulation side.
          (send state@ "FUEL DEPLETED"))  ; Warn the pilot.
        
        ; Return successor state tuple.
        (tuple altitude+ fuel+ velocity+)))
          
    ; Main loop of the lunar lander simulation.
    (define (simulation/loop state@ thruster/d)
      ; At the head of the loop we check (non-blocking) for a thruster update from the pilot.
      ; The simulation begins with the INTITIAL-STATE and a thruster setting of 0.
      (let loop ([m (duplet/try thruster/d)] [state (INITIAL-STATE)] [thruster 0])
        (cond
          ((not m) ; No thruster update was available.
           (sleep 4.0) ; Give pilot enough time to respond to state changes.
           (let* ((state+ (state/recalculate state thruster)) ; Compute successor state.
                  (summary (state/summary state+)))
             (display (format "simulation: summary ~a\n" summary))
             (send state@ summary)  ; Transmit fresh state summary to pilot.
             (if (touchdown? state+)
                 ; Was the landing soft or hard?
                 ; In either case the simulation loop terminates.
                 (if (landing/soft? state)
                     (send state@ "You've landed!")
                     (send state@ "You've crashed!"))
                 ; Otherwise lander is still descending.
                 ; Loop with the updated state and with zero thruster
                 ; if the fuel tank is empty.
                 (loop
                  (duplet/try thruster/d)
                  state+
                  (if (fuel/empty? state+) 0 thruster)))))
          (else ; Command received from the pilot.
           (let ((command (murmur/payload m))) ; Extract the command.
             (if (command/thruster? command)
                 ; Legal thruster command.
                 (cond
                   ((fuel/empty? state)
                    ; Fuel is exhausted. Warn pilot.
                    (send state@ "FUEL DEPLETED")
                    ; Loop to continue calculating state.
                    ; If pilot is skilled (or lucky) she'll have a soft landing.
                    (loop (duplet/try thruster/d) state 0))
                   (else
                    ; Some fuel remains.
                    (let ((thruster+ (cdr command)))
                      (display (format "simulation: thruster ~a\n" thruster+))
                      (loop (duplet/try thruster/d) state thruster+))))
                 
                 ; Unknown command. Just continue simulation.
                 (begin
                   (display (format "simulation: unknown command ~a\n" command))
                   (loop (duplet/try thruster/d) state thruster))))))))
  
    
     ; Higher order function that returns a lunar lander simulator as a thunk.
     ; state@: CURL from pilot for state updates from simulator.
     ; boot@: Promise CURL from pilot to obtain simulation CURL that pilot uses to
     ;       transmit thruster updates.
    (define (simulation/new state@ boot@)
      (lambda()
        (display "simulation: start\n")
        (display (format "simulation: state@ path ~a\n" (curl/path state@)))
        (display (format "simulation: boot@ path ~a\n" (curl/path boot@)))
        (let ((thruster/d (islet/curl/new '(commands) GATE/ALWAYS #f 'INTER)))
          (display (format "simulation: thruster/d ~a\n" thruster/d))
          (send boot@ (duplet/resolver thruster/d))
          (display "simulation: transmitted resolver of thruster/d\n")
          (sleep 1.0) ; Give pilot time to receive thruster CURL.
          
          ; Enter simulation loop and don't return until lander
          ; either lands safely or crashes.
          (simulation/loop state@ thruster/d)
          
          (display "simulation: game over\n")
          (display "simulation: exiting\n"))))
  
    simulation/new))
    

See the entire source code for the pilot below (or view it in Github):

  
#lang racket/base

(require
  "../../include/base.rkt"
  "../../baseline.rkt"
  [only-in "../../curl/base.rkt" curl/origin curl/path curl/metadata]
  "../../transport/gate.rkt"
  ;"../../uuid.rkt"
  "coastapp-utils.rkt")

(provide pilot)

;; CURL pre-generated by the simulation server, this grants the pilot the authority to spawn simulations
(define/CURL/inline SIM-SERVER/CURL/SPAWN
  #<<!!
SIGNATURE = #"zVMpXbsnYES0IRooE3QI4a1FpwKws-CilmEexuawFCeCKXcBpZhBHzz9vzXYqpv6nf6hYJ6_qvInhnX2YrpECg"
CURL
    id = dda12627-9e96-4efe-8d8d-8dc8896cebf6
    origin = #"RaQDnsBmoxoaCe_rkNuPJB1Q7PgSaYm17jzafmYFPSc"
    path = (service spawn)
    access/id = ingress.service.spawn
    created = "2016-06-22T16:25:22Z"
    metadata = #f

!!
  )


;; The compiled SIMULATION is transmitted from the pilot to a remote simulator island for execution.
(define SIMULATION-FACTORY
  ; Motile source follows.
  '(begin
    ; Access routines for lunar lander state tuple: .
    (define (altitude/get v) (tuple/ref v 0))
    (define (fuel/get v)     (tuple/ref v 1))
    (define (velocity/get v) (tuple/ref v 2))
    
    (define (fuel/empty? state) (= (fuel/get state) 0))
    (define (touchdown? state) (= (altitude/get state) 0))
    (define (landing/soft? state)
      (<= (velocity/get state) MAX_LANDING_VELOCITY))
    (define (command/thruster? command)
      (and
        (pair? command)
        (eq? (car command) 'THRUSTER)
        (integer? (cdr command))
        (>= (cdr command) 0)))
     
    (define (state/summary state)
      (format "Altitude: ~a Fuel: ~a Velocity: ~a" 
              (round (altitude/get state))
              (round (fuel/get state))
              (round (velocity/get state))))

    ; Initial lunar lander state #(altitude fuel velocity).
    (define (INITIAL-STATE) (tuple 1000.0 750.0 70.0))
    
    (define GRAVITY 1.4)  ; Gravitational constant.
    (define MAX_LANDING_VELOCITY 5) ; Upper bound for a soft landing.
    
    ;; Given a state tuple and a thruster value
    ;; (the number of units of fuel to burn)
    ;; calculate and return a successor state tuple.
    (define (state/recalculate state thruster)
      (let
          ((altitude+ (max 0 (- (altitude/get state) (velocity/get state)))) ; New altitude.
           (fuel+ (max 0 (- (fuel/get state) thruster))) ; Remaining fuel.
           (velocity+  ; ((velocity + GRAVITY)*10 - (thruster*2)) / 10
            (max 0 (/ (- (* (+ (velocity/get state) GRAVITY) 10) (* thruster 2)) 10))))
        
        ; Check if we just ran out of fuel.
        (when (and (= fuel+ 0) (> (fuel/get state) 0))
          (display "Fuel depleted\n")      ; Issue a notice on the simulation side.
          (send state@ "FUEL DEPLETED"))  ; Warn the pilot.
        
        ; Return successor state tuple.
        (tuple altitude+ fuel+ velocity+)))
          
    ; Main loop of the lunar lander simulation.
    (define (simulation/loop state@ thruster/d)
      ; At the head of the loop we check (non-blocking) for a thruster update from the pilot.
      ; The simulation begins with the INTITIAL-STATE and a thruster setting of 0.
      (let loop ([m (duplet/try thruster/d)] [state (INITIAL-STATE)] [thruster 0])
        (cond
          ((not m) ; No thruster update was available.
           (sleep 4.0) ; Give pilot enough time to respond to state changes.
           (let* ((state+ (state/recalculate state thruster)) ; Compute successor state.
                  (summary (state/summary state+)))
             (display (format "simulation: summary ~a\n" summary))
             (send state@ summary)  ; Transmit fresh state summary to pilot.
             (if (touchdown? state+)
                 ; Was the landing soft or hard?
                 ; In either case the simulation loop terminates.
                 (if (landing/soft? state)
                     (send state@ "You've landed!")
                     (send state@ "You've crashed!"))
                 ; Otherwise lander is still descending.
                 ; Loop with the updated state and with zero thruster
                 ; if the fuel tank is empty.
                 (loop
                  (duplet/try thruster/d)
                  state+
                  (if (fuel/empty? state+) 0 thruster)))))
          (else ; Command received from the pilot.
           (let ((command (murmur/payload m))) ; Extract the command.
             (if (command/thruster? command)
                 ; Legal thruster command.
                 (cond
                   ((fuel/empty? state)
                    ; Fuel is exhausted. Warn pilot.
                    (send state@ "FUEL DEPLETED")
                    ; Loop to continue calculating state.
                    ; If pilot is skilled (or lucky) she'll have a soft landing.
                    (loop (duplet/try thruster/d) state 0))
                   (else
                    ; Some fuel remains.
                    (let ((thruster+ (cdr command)))
                      (display (format "simulation: thruster ~a\n" thruster+))
                      (loop (duplet/try thruster/d) state thruster+))))
                 
                 ; Unknown command. Just continue simulation.
                 (begin
                   (display (format "simulation: unknown command ~a\n" command))
                   (loop (duplet/try thruster/d) state thruster))))))))
  
    
     ; Higher order function that returns a lunar lander simulator as a thunk.
     ; state@: CURL from pilot for state updates from simulator.
     ; boot@: Promise CURL from pilot to obtain simulation CURL that pilot uses to
     ;       transmit thruster updates.
    (define (simulation/new state@ boot@)
      (lambda()
        (display "simulation: start\n")
        (display (format "simulation: state@ path ~a\n" (curl/path state@)))
        (display (format "simulation: boot@ path ~a\n" (curl/path boot@)))
        (let ((thruster/d (islet/curl/new '(commands) GATE/ALWAYS #f 'INTER)))
          (display (format "simulation: thruster/d ~a\n" thruster/d))
          (send boot@ (duplet/resolver thruster/d))
          (display "simulation: transmitted resolver of thruster/d\n")
          (sleep 1.0) ; Give pilot time to receive thruster CURL.
          
          ; Enter simulation loop and don't return until lander
          ; either lands safely or crashes.
          (simulation/loop state@ thruster/d)
          
          (display "simulation: game over\n")
          (display "simulation: exiting\n"))))
  
    simulation/new))


;; Input reader service for the pilot.
(define (thruster/read thruster@)
  (display "pilot: Running input reader for thruster values\n")
  (let loop ([value (read)]) ; Prompt pilot for thruster update.
    ; Send thrust value to executing simulation.
    (send thruster@ (cons 'THRUSTER value))
    (loop (read))))

; Create a nanoservice dedicated to reading pilot input
; for the thruster.
(define (pilot/input thruster@)
  ; First create a new islet.
  (let ([x (islet/new
            (this/island)
            'pilot.input
            TRUST/MODERATE
            environ/null environ/null)])
    (islet/jumpstart x (lambda () (thruster/read thruster@)))))

;; Code for a pilot island.
;; server/u - CURL for spawn service on Sim Server.
(define (pilot/boot server/u)
  (display "pilot: booting ...\n")
  
  (display "pilot: waiting for simulator ...\n")
  (island/enter/wait (curl/origin server/u))
  (display "pilot: saw simulator\n")
  
  ; state/d: duplet (CURL/egress pair) on which pilot will receive state updates.
  ; thruster/p: promise to be resolved by simulation. Resolution will be a CURL on
  ; which pilot will send thruster values.
  ; state@: CURL on which simulator will transmit state updates.
  ; 
  (let* ([state/d (islet/curl/new '(state notify) GATE/ALWAYS #f INTER)]
         [thruster/p (promise/new)]
         [state@ (duplet/resolver state/d)]
         [boot@  (promise/resolver thruster/p)]
         [factory (island/compile SIMULATION-FACTORY)]
         [THUNK/SIMULATION
          (motile/call factory BASELINE state@ boot@)])
    
    (display "pilot: sending simulation thunk to simulator ...\n")
    (send server/u THUNK/SIMULATION)
    (display "pilot: simulation thunk sent\n") 
    
    ; Listen for curl on which to send thruster updates to simulation.
    (display "pilot: waiting for thruster curl from simulation\n")
    (let* ([m (promise/block thruster/p)]  ; Listen for the CURL.
           [thruster@ (murmur/payload m)]) ; Extract CURL for thruster updates.
      (display "pilot: received thruster CURL from simulation\n")
      (pilot/input thruster@) ; Start dedicated islet for reading pilot input.
      
      ; Listen for lunar lander state events.
      (let loop ([m (duplet/block state/d)])
        (let ([state+ (murmur/payload m)]) ; Extract the murmur's payload.
          (display state+)(newline))
        (loop (duplet/block state/d))))))

; Construct an in-memory CURL instance of the predefined CURL for sim-server.
(define sim-server/curl/spawn (curl/zpl/safe-to-curl SIM-SERVER/CURL/SPAWN KEYSTORE))

; fire up the pilot island
(define pilot
  (example/island/new
   'pilot          ; Name of pilot island.
   "pilot_secret"  ; Filename of island certificate.
   (lambda ()      ; Bootstrap thunk for island.
     (pilot/boot sim-server/curl/spawn))))

(island/log/level/set 'warning)

        

The Similation Server Binding Environment

Although in this simple example the simulation server does not provide any particular services related to a lunar lander simulation, the application does require some COAST and Racket functionality that is not provided in a default COAST binding environment. Here we define a new binding environment for our simulations, SIMULATOR/ENVIRONMENT, that includes functions that a simulation needs to operate.

See the entire source code for the simulation server binding environment below (or view it in Github):


#lang racket/base

(require
  "../../baseline.rkt"
  "../../persistent/environ.rkt"
  "../../promise.rkt"
  "../../murmur.rkt"
  "../../send.rkt"
  "../../transport/gate.rkt"
  "../../islet.rkt"
  )

(provide SIMULATOR/ENVIRON)

(define SIMULATOR/ENVIRON
  (pairs-to-environ
   BASELINE/SPAWN 
   (list
    (define/global/0 'newline newline)
    (define/global/1 'duplet/resolver duplet/resolver)
    (define/global/1 'duplet/block duplet/block)
    (define/global/1 'murmur/payload murmur/payload)
    (define/global/N 'islet/CURL/new islet/CURL/new)
    (define/global/N 'motile/call motile/call)
    (define/global/1 'sleep sleep)
    (cons 'GATE/ALWAYS GATE/ALWAYS)
    (define/global/2 'send send)
    
    )))
    

Island/Keystore Utilities

This utility code defines a global kesytore and provides a new island creator function, examples/island/new, for COAST example applications that both creates a new island and sets its keystore. See CURLs getting real for a full explanation of island creation and keystores.

See the entire source code for coast application utilities for keystores below (or view it in Github):


#lang racket/base

(require
  racket/contract/base
  "../../include/base.rkt")

(provide
 (contract-out
  
  [example/island/new (-> symbol? string? procedure? island?)])
 
 CERTIFICATE/PUBLIC
 CERTIFICATE/SECRET
 KEYSTORE)


(define CERTIFICATE/PUBLIC "./certificates/public/")

(define CERTIFICATE/SECRET "./certificates/secret/")

(define KEYSTORE ((lambda () 
                   (let ([k (keystore/new)])
                     (keystore/load k CERTIFICATE/PUBLIC)
                     k))))

(define (example/island/new nickname filename bootstrap)
  (let* ([ISLAND/SECRET/PATH   (string-append CERTIFICATE/SECRET filename)]
         [ISLAND/CURVE/SECRET   (path-to-curve ISLAND/SECRET/PATH)]
         [island (island/new nickname ISLAND/CURVE/SECRET bootstrap)])
    (island/keystore/set island KEYSTORE)
    island)
  )

    

Back to the top

This material is based upon work supported by the National Science Foundation under Grant Numbers CNS-1449159, CCF-0820222 and CCF-0917129. Any opinions, findings, and conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the National Science Foundation.