# Web API for the use by the Rule Game client - second batch

Version 4.000, 2021-10-20

This is the "Second Batch" (prefix=`/GameService2`) of the Game Service web API calls. They are intended for use with a client app that works with players enrolled in experiments; each player is identified by a player ID, and the server assigns each player to a "trial list" (a particular track in the experiment plan with which this player ID is associated with).

For calls that can be used by players not enrolled in experiments (e.g. our own team members developing and testing games), see the First Batch.

1. GET `/getVersion:` Report the currently deployed version of the Game Server.
2. POST `/registerUser:` Regsiter a new repeat user. The Android app should do it when it's first used, before any games are applied. Before making this call, the Androing app can ask the user to either identify himself in some way (by entering an email address and/or a nickname), or to proceed anonymously (by checking the "anonymous" box on the identification screen). Email:
   Nickname:
   Anonymous:

   On error, the return structure may have an error flag set to true, and an error message. On success, the return structure will contain an integer user id (`user.id`), which the Android app should save for later use in /player calls. Another value, `user.idCode` should be remembered as well; in future versions, it may be used for authentication. (As of ver 4.000, this is not yet supported).

   Sample return value, when a new user is registered with a nickname and no email (thus, `newlyRegistered=true`) {"error":false,"newlyRegistered":true,"user":{"date":"2021-10-20T18:38:31.288Z[UTC]","email":"","id":6,"idCode":"user-20211020-143831-GF0LJI","nickname":"The Walrus"}}

   Sample return value, when a new user is registered with a nickname and an email address (thus, `newlyRegistered=true`) `{"error":false,"newlyRegistered":true,"user":{"date":"2021-10-20T18:40:19.968Z[UTC]","email":"v.menkov@gmail.com","id":7,"idCode":"user-20211020-144019-I9O244","nickname":"The Walrus"}}`

   Sample return value, when a new user is registered with no email or nickname, and with the "anon" box checked:`{"error":false,"newlyRegistered":true,"user":{"date":"2021-10-20T18:45:44.047Z[UTC]","id":8,"idCode":"anon-20211020-144544-9WGVH3"}}`

   Sample return value, when the email and nickname matched an already regiesterd (thus, `newlyRegistered=false`) `{"error":false,"newlyRegistered":false,"user":{"date":"2021-10-12T01:55:21Z[UTC]","email":"vmenkov@gmail.com","id":3,"idCode":"user-5L54KP","nickname":"V Menkov Test 1"}}`

3. POST `/player:` Start a player's interaction with the system, and obtain a randomly selected trial list for this player. Player ID: -- mandatory, unless Repeat User ID is supplied (in which case the server will create a unique playerId)
   Experiment plan (optional, e.g `qt` or `pk1`): -- controls the assignment of the player to an experiment plan. Mandatory (unless you want an assignment based on the player name, which usually makes litlle sense).
   Repeat User ID: -- a numerical user ID, if the player belongs to a repeat user. Leave blank otherwise

   The input should include either player ID (pid), or Repeat User ID (uid), or both. In the traditional approach with no repeat users (e.g., the Mechanical Turk use case) only the player ID is supplied. In the repeat-user context (e.g. from the Android app), it is best to supply only the uid; the server will then create a unique pid and return it. Sample output:

         {"errmsg":"Debug:\n*M*[0]\n[1]\n[2]\nid=46, curSer=0 b=false, R=$0"
         ,"error":false,"alreadyFinished":false,
         "experimentPlan":"P:pilot06:no-feedback",
         "newlyRegistered":true,
         "playerId":"RepeatUser-8-V7CMVI-20211020-144752",

         "trialList":[{"clearing_threshold":1.3,"max_points":10,"b":1.5,
         "min_points":2,"max_colors":4,"feedback_switches":"free",
         "min_objects":9,"clear_how_many":2,"bonus_extra_pts":3,"rule_id":
         "shapeMatch_1","max_objects":9,"grid_memory_show_order":false,
         "min_shapes":4,"give_up_at":3,"stack_memory_show_order":false,
         "max_shapes":4,"min_colors":4,"stack_memory_depth":0,"max_boards":5,
         "activate_bonus_at":3},
         ....],
         "trialListId":"shape_shapeBroad_posBroad"}

   For the meaning of the output fields, see the documentation for PlayerResponse. A field named `foo` will be described there under `getFoo()` or (for boolean values) `isFoo()`. The field named `playerId` normally simply mirrors the input value of the same name; but if playerId was blank and user ID was supplied instead, the server will create a playerId itself, and return it via this field.

   Ver. 1.021: added `trialList`, which is a vector with one element per line of the trial list. Can be used to find the total number of rule sets to which a player will be exposed.

   Sample output:

             {"errmsg":"Debug:...",
             "error":false,
             "newlyRegistered":false,
             "trialListId":"trial_1"}

   In practice, if you supplied a pid with the input you don't need any of the returned values, other than the error code. If uid with no pid was supplied on input, you need to save the pid to use it in the subsequent calls to /newEpisode or /mostRecentEpisode.
4. POST `/mostRecentEpisode`: Return the latest episode (complete or incomplete) for a specified user: Player ID:

   The response is in the same format as for /newEpisode, below. Check the values of `display.finishCode` to see the status of the game (continuing/cleared/stalemated/given up), and of `display.guessSaved` to see if a guess has been recorded.

5. POST `/newEpisode`: Create a new episode for a specified user: Player ID:
   Optional parameters (to effect certain preliminary actions before the /newEpisode call):
   Activate Bonus:
   (If true, the /activateBonus operation will be executed immediately prior to the new episode retrieval)
   Give up:
   (If true, the /giveUp operation will be executed immediately prior to the new episode retrieval)

   For the meaning of the output fields, see the documentation for NewEpisodeWrapper2. A field named `foo` will be described there under `getFoo()` or (for boolean values) `isFoo()`.

   Sample output; note that some fields have been moved from X to display.X. The fields within display.{...} are the same as those given by the /display and /move calls.

       	  {"errmsg":"Debug:...",
       	  "error":false,
       	  "alreadyFinished":false,
       	  "display":{
       	  "board":{"id":0,"value":[{"buckets":[3],"color":"BLUE","id":1,"shape":"SQUARE","x":5,"y":5}]},"code":-8,"errmsg":"Display requested\nDEBUG\n[0][20200827-153052-F66ITP; FC=1; M 2/2 $10][20200827-153246-R7LHH9; FC=1; M 3/3 $10]\n[1][20200827-160602-PYQTAJ; FC=1; M 2/2 $10][20200827-162713-2HRENR; FC=1; M 2/2 $10]\n*M*[2][20200905-120700-PDF966; FC=1; M 1/1 $10][20200905-121136-7L3EHY; FC=0; M 0/1 $0]\nR=$50",
       	  "finishCode":0,
       	  "numMovesMade":1,
       	  "bonus":false,
       	  "bonusEpisodeNo":0,
       	  "canActivateBonus":true,
       	  "episodeNo":1,
       	  "guessSaved":false,
       	  "seriesNo":2,
       	  "totalBoardsPredicted":2,
       	  "totalRewardEarned":50
       	  "transcript":[{"pos":32,"bucketNo":3,"code":0}]} // All moves (and move attempts) - new in ver 1.014
       	  },
       	  "ruleSetName":"TD-03",
       	  "trialListId":"trial_list_1",
       	  "episodeId":"20200905-121136-7L3EHY",
       	  "para":{"clearing_threshold":1.3,"max_points":10,"b":1.5,"min_points":2,"max_colors":1,"f":4,"feedback_switches":"fixed","min_objects":1,"m":9,"n":1,"clear_how_many":2,"bonus_extra_pts":3,"rule_id":"TD-03","max_objects":3,"grid_memory_show_order":false,"min_shapes":1,"stack_memory_show_order":false,"max_shapes":1,"min_colors":1,"stack_memory_depth":6,"max_boards":2,"activate_bonus_at":2}}

   display.transcript contains the description of all previous moves, as a vector. Each element has pos=the origin position (as an integer, numbered from 1=(1,1) to 36=(6,6), by row from left bottom corner), bucketNo=the destination bucket number, and the success code (same meaning as display.code)
   Sample output

         {"errmsg":"Debug...",  // either used to describe errors, or contains various status info for debugging.
         "error":false,    // true if something bad has happened
         "alreadyFinished":false,   // this will be true if error==true, and the error is caused by the fact that the player has already done all his param sets
        "board":{"longId":0,"id":0,"value":
       [{"buckets":[0],"color":"RED","id":0,"shape":"STAR","x":3,"y":1},
       {"buckets":[0],"color":"RED","id":0,"shape":"STAR","x":3,"y":5}]},
        // description of the initial board
       "bonus":false,     // true if this episode is part of a bonus subseries
       "bonusEpisodeNo":0,  // the number of previous bonus episodes in this series
       "canActivateBonus":false,  // true if you can put an "Activate Bonus" button on the screen in this episode
       "trialListId":"trial_list_1",  Which trial list from the current experiment plan has been chosen for this player?
       "ruleSetName":"TD-03",    The name of the rule set contained in the current parameter set. This is a file path name, relative to the main rule set directory
       "episodeId":"20200828-021427-FJRX9J", // Save this and use it in /move calls later!
       "para":
       {"clearing_threshold":1.3,"max_points":10,"b":1.5,"min_points":2,
       "max_colors":1,"f":4,"feedback_switches":"fixed","min_objects":1,"m":9,"n":1,
       "clear_how_many":2,"bonus_extra_pts":3,"rule_id":"TD-02","max_objects":3,
       "grid_memory_show_order":false,"min_shapes":1,"stack_memory_show_order":false,
       "max_shapes":1,"min_colors":1,"stack_memory_depth":6,"max_boards":2,
       "activate_bonus_at":2},
        // All the parameters from the current param set
         //  (the current row of the trial list file). Some of them may be used by the
       //GUI tool to control some points of the appearance
       "seriesNo":1,  // the 0-based sequential number of this series (= 0-based sequential number of this row of the trial list file).
       // You can increment this number by 1 and display it in a "You are working on Rule No. X" message.
       "episodeNo":0, // the 0-based number of this episode within the series (i.e. among
       // the episodes played under a given rule set).
       // You can increment this number by 1 and display it as Y in a "Rule No. X, Episode No. Y" message.
       "totalRewardEarned":33  // the sum total of rewards earned by this player in all episodes so far
       }

       As of ver. 1.011, in the board description the field dropped is shown only in the pieces that have been already removed from the board; the value is the ientifying number (0 thru 3) of the bucket into which the piece has been dropped.

6. GET `/display`: View the current display: Episode ID (returned by an earlier newEpisode call):

   For the meaning of the output fields, see the documentation for ExtendedDisplay. A field named `foo` will be described there under `getFoo()` or (for boolean values) `isFoo()`.

   Sample output (in ver. 1.016). The new fields are ruleSrc and ruleLineNo.

         {"bonus":false,
         "totalRewardEarned":0,
         "seriesNo":0,"episodeNo":0,
         "bonusEpisodeNo":0,
         "canActivateBonus":false,
         "totalBoardsPredicted":2,
         "guessSaved":false,
         "rulesSrc":{  // The rules of this game
         "orders":   // array of orders. In this specific game (TD-1) one order is defined, but is not actually used
            ["Order Custom=[36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1]"],
         "rows": // array of rules, one element per line
            ["(1,*,*,*,[0,1,2,3])",
             "(*,*,*,*,[((p + 3) % 4)])"]
         },
         "ruleLineNo":0,  //  which line of the rules set is currently active (0-based)
         "finishCode":0,
         "board":{"id":0,"value":[{"id":1,"color":"RED","shape":"SQUARE","x":6,"y":3,"buckets":[0,1,2,3]},{"id":0,"color":"RED","shape":"SQUARE","x":1,"y":1,"dropped":3,"buckets":[0,1,2,3]}]},
         "code":-8,  // It is always -8 on /display calls
         "errmsg":"null\nDEBUG\n*M*[0][20200910-005354-0JVTXF; FC=0; m 1/2 $0]\n[1]\n[2]\nR=$0","numMovesMade":1,
         "transcript":[{"pos":1,"bucketNo":3,"code":0}]
         }

   Sample output (in ver. 1.012), extra fields added (some of which pevious were only seen in /newEpisode):

         {"board":{"id":0,"value":[{"buckets":[3],"color":"BLUE","dropped":3,"id":0,"shape":"SQUARE","x":5,"y":5}]},
         "code":-8,
          "errmsg":"Display requested\nDEBUG\n[0][20200827-153052-F66ITP; FC=1; M 2/2 $10][20200827-153246-R7LHH9; FC=1; M 3/3 $10]\n[1][20200827-160602-PYQTAJ; FC=1; M 2/2 $10][20200827-162713-2HRENR; FC=1; M 2/2 $10]\n*B*[2][20200905-120700-PDF966; FC=1; M 1/1 $10][20200905-121136-7L3EHY; FC=1; B 1/1 $10]\nR=$60", // just a progress report for this session, handy for debugging
         "finishCode":1,
         "numMovesMade":1,
         "bonus":true,
         "bonusEpisodeNo":0,
         "canActivateBonus":false,
         "episodeNo":1, // sequential number in this series (0-based)
         "guessSaved":false, // has the guess been recorded for this episode?
         "seriesNo":2, // sequential number of this series in the trial list (0-based)
         "totalBoardsPredicted":3, // The current prediction for the total number of episodes to be played in this series.
           // During the main subserties, this is simply para.max_boards; during the bonus subseries, this is A+para.clear_how_many,
           // where A is the number of non-bonus episodes that had been played in this series before bonus was activated.
         "totalRewardEarned":60}

   Sample output (in ver. 1.011)

         {"board":{"longId":0,"id":0,"value":
         [{"buckets":[0],"color":"RED","dropped":1,"id":0,"shape":"STAR","x":3,"y":1},
         {"buckets":[0],"color":"RED","dropped":3,"id":0,"shape":"STAR","x":3,"y":5}]},
         "bonus":false,   // true if this episode is part of a bonus subseries
         "code":-8,  // It is always -8 on /display calls
         "errmsg":"Display requested",
         "finishCode":1, // Tells if the episode still continues, has stalemated,
       //    has been finished by clearing the board, or has been given up.
       //    For the semantics of the code and  finishCode fields,
       //    see the entry on the MOVE command in the documentation on the Captive Game Server
         "numMovesMade":3,  // the number of move attempts made so far in this episode
         "totalRewardEarned":0  // the sum total of rewards earned by this player in all episodes so far
         }

   Two more fields added in ver. 2.007:

       "trialListId":"trial_list_1", // Which trial list from the current experiment plan has been chosen for this player?
       "ruleSetName":"TD-03",  //doc  The name of the rule set contained in the current parameter set. This is a file path name, relative to the main rule set directory

   If the rule set file was specified in the param set as an absolute path, that's what you'll see here as well:

       "ruleSetName":"/home/vmenkov/test/rules/rule-a.txt",

7. POST `/move`: Make a move: Episode ID (returned by an earlier newEpisode call):
   Piece to move: column=x=, row=y= (both ranging 1 thru 6)
   Destination bucket (identified by bucket_column=bx=, bucket_row=by= (possible values are 0 and 7)
   Attempted move and pick count so far (the number of previous calls to `move` and `pick` in this episode. This can also be interpreted as the 0-based sequential number of this pick or move attempt)

   The response structure is nearly identical to that of /move, except that `code` contains a value that describes the accept/deny code to this move. For the meaning of the output fields, see the documentation for ExtendedDisplay. A field named `foo` will be described there under `getFoo()` or (for boolean values) `isFoo()`.

   Sample response

         {"board":{"longId":0,"id":0,"value":
         [{"buckets":[1],"color":"BLUE","id":0,"shape":"TRIANGLE","x":4,"y":2},
         {"buckets":[1],"color":"BLUE","dropped":3,"id":0,"shape":"TRIANGLE","x":2,"y":3}]},  // the board after this move
         "bonus":false,  // true if this episode is part of a bonus subseries
         "code":4,  Outcome of this move.
           For the semantics of the code and  finishCode fields,
           see the entry on the MOVE command in the documentation on the Captive Game Server
         "errmsg":"...", // either used to describe errors, or contains various status info for debugging.
         "finishCode":0,  Overall state of this episode now.
           For the semantics of the code and  finishCode fields,
           see the entry on the MOVE command in the documentation on the Captive Game Server
         "numMovesMade":1,  // the number of move attempts made so far in this episode
         "totalRewardEarned":42 // the sum total of rewards earned by this player in all episodes so far
         }

   The finish code reports the current state: 0=can play (there are pieces on the board, and some can be moved), 1=finish (no pieces left on the board), 2=stalemate (there are pieces on the board, but none of them can be moved anymore).

8. POST `/pick`: Pick a piece: Episode ID (returned by an earlier newEpisode call):
   Piece to move: column=x=, row=y= (both ranging 1 thru 6)
   Attempted move and pick count so far (the number of previous calls to `move` and `pick` in this episode. This can also be interpreted as the 0-based sequential number of this pick or move attempt)
   The arguments to the `/pick` call are similar to those used in a /move call, except that there are no bucket coordinates. Unlike /move, a /pick call merely asks whether the piece in question can be moved to any position, but does not actually move it to anywhere. The return value is the same JSON structure as a move call would return; look at the `code` field, with the same ACCEPT/DENY semantics. Here, ACCEPT (code 0) means that the piece can be moved to at least one bucket; DENY (code 4) indicates that the piece cannot be moved to any bucket.
9. POST `/activateBonus:` Activate the bonus for this player. The current episode (if still active) and all subsequent episode of the same series (i.e. played by the same rule) will constitute the "bonus subseries"
   Sample output

           {"errmsg":"Bonus activated successfully","error":false}

The finish code reports the current state: 0=can play (there are pieces on the board, and some can be moved), 1=finish (no pieces left on the board), 2=stalemate (there are pieces on the board, but none of them can be moved anymore).

10. POST `/giveUp:` "Gives up" the current series (i.e. the series played with the current param set), so that the next episode will be run with the next param set. If this is invoked while an episode is in progress, the episode will be marked as "given up" as well. However, you may choose to only use this call at the end of an episode (that is, give the player an option of switching to the next param set for the next episode, regardless of one's current position in the current series). Player ID: Series No.:
    The seriesNo value must be exactly the same to which the current (or just completed) episode belongs to, as per the "seriesNo" value obtained in the last /newEpisode call. This is required as a safety check, to ensure that the client and server are on the same page. (If it's an annoyance to have it, I can remove this parameter)
    Sample output (on success):

        {"errmsg":"Gave up series 0","error":false}

11. POST `/guess:` Recording a guess Episode ID (returned by an earlier newEpisode call):
    Please type your guess here (an arbitrary test)
    Confidence

    Response (on success):

        {"error":false,"byteCnt":91,"path":"/opt/tomcat/saved/guesses/qt-01.csv"}

    For the meaning of the output fields, see the documentation for GuessWriteReport. A field named `foo` will be described there under `getFoo()` or (for boolean values) `isFoo()`.

    The response contains a structure, `transitionMap`, which can be used to create a set of button offering the player possible further actions. The structure describes buttons in terms of key/value pairs, where the key tells where you'll get if you click the button, and the value tells how you can get there. Listed below are all possible key/value pairs, and their semantics.

    Key | Value | How the button can be labeled | What the client can if after player clicks the button
    --- | --- | --- | ---
    MAIN | DEFAULT | Next episode | Call /newEpisode, which will produce another main-subseries (non-bonus) episode in the same series
    BONUS | DEFAULT | Next episode | Call /newEpisode, which will produce another bonus episode in the same series
    NEXT | DEFAULT | Next series | Call /newEpisode, which will automatically start the first episode of the next series (because no more episodes can be started in the current series)
    END | DEFAULT | Finish | The end of the last series, and of the experiment. Call /newEpisode. It will return error=true, allFinished=true, and completionCode, which the GUI client can then display to the player
    BONUS | ACTIVATE | Activate Bonus | Call /activateBonus, followed by /newEpisode (or simply call /newEpisode with activateBonus=true), which will start the first bonus episode in this series
    NEXT | GIVE_UP | Give up (and proceed to the next series) | Call /giveUp (which will switch series) followed by /newEpisode. (Or simply call /newEpisode with giveUp=true). The first episode of the next series will start
    END | GIVE_UP | Give up (and end the last series, and the experiment) | Call /newEpisode with giveUp=true. It will return error=true, allFinished=true, and completionCode, which the GUI client can then display to the player.

    A button with value="DEFAULT" is produced after every episode, indicating the default action (continuing in the same series, switching to the next series if the current series is done, or finishing one's work after the last series). A button with value="GIVE_UP" is produced after every episode other than the last episode of a series; it indicates the player's ability to end a series prematurely (and then go to the next series, if available). A button with value="ACTIVATE" is produced after main-subseries episodes if the player is eligibile to switch to ("activate") the bonus subseries.

12. GET `/debug`: Same info as given by /player, and some more for a specified user: Player ID:
13. GET `/colorMap`: shows the currently supported colors. Return value: a hash map; each keys is a color name (in uppercase), and the corresponding value is a vector of 3 integers (each number in the [0:255] ranges), describing the color in the RGB space. The data are taken from /opt/tomcat/game-data/colors/colors.csv. E.g.

        	  {"RED":[220,20,0],
        	  "PINK":[220,100,100],
        	  "BLUE":[30,90,210],
        	  "errmsg":"No error",
        	  "BLACK":[0,0,0],
        	  "YELLOW":[250,240,0],
        	  "PURPLE":[200,0,200],
        	  "error":false,
        	  "GREEN":[0,250,0]}

    The hash table may also contain fields "error" and "errmsg". If an error has happened (e.g. the color map file on server could not be parsed), the error field will be set to true, and errmsg will contain an explanation of sorts.
14. GET `/listShapes`: shows the currently supported shapes. This should return a structure with the field named `value` whose value is a list of shapes. E.g.:

        {"error":false,
        "values":["cloud-lightning","moon","blank","cloud-off","sun","cloud","cloud-rain","cloud-drizzle","triangle","star","cloud-snow","arrows/arrow-up","arrows/arrow-left","arrows/arrow-down-left","arrows/arrow-down-right","arrows/arrow-up-left","arrows/arrow-right","arrows/arrow-up-right","arrows/arrow-down","square","circle"]}

    The shape names are given using the same case as in the file names. They are not case-sensitive, though.
    If an error happens, a structure with an error field is returned instead:

        {"error":true, "errmsg": "And this is why: ..."}

15. (Effective ver. 3.0) GET `GetImageServlet` -- Get an image file: This should return an image file (in SVG, PNG, JPEG, or GIF format, e.g. /opt/tomcat/game-data/shapes/star.svg), with the content type as approprite for the file type (`image/svg+xml, image/jpeg` etc). In the case of an error (e.g. no such file), error code 404 will be returned.
16. (Obsolete as of ver. 3.0)GET `admin/getSvg.jsp` -- Get SVG file: This should return an SVG file (e.g. /opt/tomcat/game-data/shapes/star.svg for argument STAR), with content type image/svg+xml. In the case of an error (e.g. no such file), error code 404 will be returned.

## HTML play

The "HTML play" can be used to play some episodes in the web browser, without the use of the client GUI app. This allows to test the Game Server functionality separately from that of the client GUI app.

Note: for technical reasons, in the HTML play the game pieces are drawn in black, and their assigned color (slightly "watered down", for better visibility) is indicated by the background of the cell. This is different from how the pieces appear in the GUI client.

- POST `/playerHtml`: Register a new player in an experiment plan of your choice: Player ID:
  Experiment plan (`vmColorTest` or `default`):
- POST `/mostRecentEpisodeHtml`: Look up the most recent episode for a specified user: Player ID:
- POST `/newEpisodeHtml`: Create a new episode for a specified user: Player ID:
- /moveHtml Episode ID (returned by an earlier newEpisode call):
  Piece to move: column=x=, row=y= (both ranging 1 thru 6)
  Destination bucket (identified by bucket_column=bx=, bucket_row=by= (possible values are 0 and 7)
  Attempted move count so far (the number of previous calls to `move` in this episode. This can also be interpreted as 0-based number of this move attempt)

## Clear tables

For efficiency's sake, the Game Service tends to cache certain data, such as parsed rule sets or object property tables. If you modify a trial list file or a rule set file that already exists under `/opt/tomcat/game-data`, you may want to force the server to purge its stored tables, to ensure that your updated files will be re-read. If any players are playing at this time, clearing the table may cause a slight delay for them (as all necessary tables are re-read), but no disruption of their games.

## Validate your experiment plan

You can use the experiment plan validation page to check your experiment plan's experiment control files for errors (syntax errors, references to non-existent files, attempts to generate boards with non-existence colors or shapes, etc). Or if you remember the name of your plan, you can do it right here; just enter the name of your experiment plan and click on "Submit". Experiment plan (e.g `vmColorTest` or `default`):

## Where are the input files?

In /opt/tomcat/game-data

## Where are the output files?

In /opt/tomcat/saved

[HOME]