Skip to content

Commit 1a63114

Browse files
committed
Removed path parameter from Welcome opcode
1 parent 89a8326 commit 1a63114

11 files changed

+37
-701
lines changed

Scalatron/Readme.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ in the public domain. Feel free to use, copy, and improve them!
5454

5555
## Version History
5656

57+
### Version 1.0.1.2 -- 2012-05-21
58+
59+
* Protocol change: `Welcome` opcode no longer provides parameter `path`.
60+
61+
5762
### Version 1.0.1.1 -- 2012-05-21
5863

5964
* Minor fix to facilitate migration of existing user accounts (created with 1.0.0.2 or earlier) to git-based versioning.

Scalatron/doc/markdown/Scalatron Protocol.md

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ These are the opcodes valid for commands sent by the server to a plug-in's contr
246246
Only one opcode will be present per control function invocation.
247247

248248

249-
### Welcome(name=String,path=string,apocalypse=int,round=int)
249+
### Welcome(name=String,apocalypse=int,round=int)
250250

251251
"Welcome" is the first command sent by the server to a plug-in before any other invocations of
252252
the control function.
@@ -255,16 +255,6 @@ Parameters:
255255

256256
* `name`: the player name associated with the plug-in. The player name is set based on the
257257
name of the directory containing the plug-in.
258-
* `path`: the path of the directory from which the server loaded the plug-in (which the plug-in
259-
would otherwise have no way of knowing about, aside from a hard-coded string).
260-
Contains no terminating slash. The plug-in can store this path and create log-files
261-
in it, ideally incorporating the
262-
round index provided in the `round` parameter and optionally the `time` (step index)
263-
and entity `name` passed with each later `React` command.
264-
**Note**: copious logging will
265-
slow down gameplay, so be reasonable and restrict logging to specific rounds
266-
and steps. For suggestions, refer to the *Debugging* section in the
267-
*Scalatron Tutorial*.
268258
* `apocalypse`: the number of steps that will be performed in the upcoming game round. This
269259
allows bots to plan ahead and to e.g. schedule the recall of harvesting drones.
270260
Keep in mind, however, that the control function of master bots is only invoked

Scalatron/doc/markdown/Scalatron Tutorial.md

Lines changed: 5 additions & 336 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,266 +2582,6 @@ aberrations. The Scalatron protocol provides several mechanisms for doing this:
25822582
state parameters which the server maintains for it. These state parameters can be inspected
25832583
in the browser-based Scalatron IDE while single-stepping through a sandboxed simulation.
25842584

2585-
* **Displaying a window** -- open a window (e.g. using an AWT Frame) and render some debug
2586-
information in real-time. This makes sense only if your code uses complex data structures that
2587-
would be too cumbersome to analyze in a log file. Note also that while you can, of course, do
2588-
this on a locally running test game server at any time, on the shared game server whose display
2589-
is projected for everyone to see this is pretty intrusive.
2590-
2591-
* **Logging to disk** -- in the `Welcome` message sent by the server, record the path to the
2592-
directory in which your bot plug-in resides *as seen by the server*. Then add debug logging
2593-
instructions to your code that make the state of your program visible at critical points by
2594-
writing into a log file in that directory. This technique is explained in detail soon via a
2595-
bot example.
2596-
2597-
2598-
2599-
2600-
## Example Bot: Debug Logger
2601-
2602-
### Objective
2603-
2604-
This section describes how to log debug information to disk while your bot is running inside
2605-
a multi-player tournament on a central game server. One reason this crude debugging mechanism
2606-
is occasionally useful is that your code is executed remotely on another compute inside a
2607-
Java Virtual Machine that you have no access to. There is therefore no way to set breakpoints,
2608-
to single-step through your code or to examine the values of variables at run-time, as you
2609-
would with a local debugger.
2610-
2611-
Note that this debugging approach is a measure of last resort. If you are working in the
2612-
browser-based Scalatron IDE or if you are using a local IDE and debugging your bot in a
2613-
local server is sufficient, you can skim this example and focus only on the discussion of
2614-
the Scala code for its own sake.
2615-
2616-
2617-
2618-
### Debug Logging
2619-
2620-
Since your code is running alongside multiple other bots on the server, simply sending
2621-
output to the console via `println` is not a viable option, except for extremely rare
2622-
messages. Log output would scroll past way too quickly and no-one would be able to see
2623-
what is going on.
2624-
2625-
The recommended approach is therefore to log your debug output into a plug-in specific
2626-
log file. But where to place that file? Your plug-in will need to use a path that is
2627-
valid in the context where it is being executed (i.e., on the computer where the game
2628-
server is running). To keep things simple and to avoid having you hard-code paths into
2629-
your plug-in, the server provides the path of the directory from which your plug-in was
2630-
loaded as a parameter to the "Welcome" command:
2631-
2632-
Welcome(name=string,path=string,round=int)
2633-
2634-
You can cache the `path` and `round` values for later in fields of your bot class.
2635-
2636-
A second issue is logging overhead. If every plug-in were to log lots of data every step
2637-
of every round for every entity (master bot and mini-bots), there is a risk that the
2638-
game server would experience significant slowdown and the experience would be spoiled
2639-
for everyone. It is therefore recommended that you log information only during certain
2640-
steps of the simulation (say, at step zero or every 100th step). It is up to you whether
2641-
you append multiple data points to a single log file or whether you generate separate log
2642-
files for each such log-relevant event.
2643-
2644-
The example code below illustrates how one might do this.
2645-
2646-
2647-
2648-
### Source Code
2649-
2650-
import java.io.FileWriter
2651-
2652-
// omitted: class ControlFunctionFactory
2653-
// omitted: object CommandParser
2654-
2655-
class ControlFunction() {
2656-
var pathAndRoundOpt : Option[(String,Int)] = None
2657-
2658-
def respond(input: String): String = {
2659-
val (opcode, paramMap) = CommandParser.apply(input)
2660-
2661-
opcode match {
2662-
case "Welcome" =>
2663-
// first call made by the server.
2664-
// We record the plug-in path and round index.
2665-
val path = paramMap("path")
2666-
val round = paramMap("round").toInt
2667-
pathAndRoundOpt = Some((path,round))
2668-
2669-
case "React" =>
2670-
// called once per entity per simulation step.
2671-
// We check the step index; if it is a multiple of
2672-
// 100, we log our input into a file.
2673-
val stepIndex = paramMap("time").toInt
2674-
if((stepIndex % 100) == 0) {
2675-
val name = paramMap("name")
2676-
pathAndRoundOpt.foreach(pathAndRound => {
2677-
val (dirPath,roundIndex) = pathAndRound
2678-
val filePath = dirPath + "/" +
2679-
name + "_" +
2680-
roundIndex + "_" +
2681-
stepIndex + ".log"
2682-
val logFile = new FileWriter(filePath)
2683-
2684-
logFile.append(input) // log the input
2685-
2686-
// if we logged more stuff, we might want an occasional newline:
2687-
logFile.append('\n')
2688-
2689-
// close the log file to flush what was written
2690-
logFile.close()
2691-
})
2692-
}
2693-
2694-
case "Goodbye" =>
2695-
// last call made by the server. Nothing to do for us.
2696-
2697-
case _ =>
2698-
// plug-ins should simply ignore unknown command opcodes
2699-
}
2700-
"" // return an empty string
2701-
}
2702-
}
2703-
2704-
2705-
2706-
### What is going on?
2707-
2708-
We added a field to the `ControlFunction` class that will hold the directory path and the round
2709-
index as an `Option[Tuple2]`:
2710-
2711-
var pathAndRoundOpt : Option[(String,Int)] = None
2712-
2713-
Initially, the field will hold the value `None`. Once the server tells us what the actual
2714-
values should be, the field will hold a `Some` value storing the plug-in directory path and
2715-
the round index as a pair. We'll explain the reason for the explicit type specification later.
2716-
2717-
Instead of the earlier `if` clause that tested for just the "React" opcode, we now want to
2718-
distinguish multiple possible values (and potentially complain about unknown opcodes). So a `match`
2719-
expression is better suited. In it, we distinguish between the three opcodes "Welcome",
2720-
"React" and "Goodbye" (see the *Scalatron Protocol* documentation for details).
2721-
2722-
In the opcode handler for "Welcome", we extract the values the server gives us for the
2723-
directory path and the round index and update the field:
2724-
2725-
val path = paramMap("path")
2726-
val round = paramMap("round").toInt
2727-
pathAndRoundOpt = Some((path,round))
2728-
2729-
In the opcode handler for "React", we extract the index of the current simulation step:
2730-
2731-
val stepIndex = paramMap("time").toInt
2732-
2733-
and test whether it is evenly divisible by 100:
2734-
2735-
if((stepIndex % 100) == 0) {
2736-
2737-
The conditional code will therefore be executed only every 100th simulation step.
2738-
In it, we extract the name of the entity for which the server is invoking the control function
2739-
(for a master bot this will be the name of the plug-in; for mini-bots it will be whatever the
2740-
plug-in told the server their name should be in `Spawn`):
2741-
2742-
val name = paramMap("name")
2743-
2744-
we then test whether the optional value `pathAndRoundOpt` was set (using a `foreach`
2745-
instead of `match` or `if`, a technique we'll explain in just a second) and, if so, we
2746-
extract the plugin directory path and the round index into two local values `dirPath` and
2747-
`roundIndex` (using the extraction syntax for `Tuple2` we already saw earlier)
2748-
2749-
val (dirPath,roundIndex) = pathAndRound
2750-
2751-
and then construct a log file name that incorporates all of the distinguishing elements
2752-
of the debug "event": the entity name, the round index and the step index. Since the file
2753-
resides in the plug-in's directory, there is no need to incorporate the plug-in name.
2754-
2755-
val filePath = dirPath + "/" +
2756-
name + "_" +
2757-
roundIndex + "_" +
2758-
stepIndex + ".log"
2759-
2760-
We then create a `FileWriter` instance for the log file
2761-
2762-
val logFile = new FileWriter(filePath)
2763-
2764-
Now we can log into the file whatever we want. The example just logs the command
2765-
the server sent, plus a newline (just so you know how):
2766-
2767-
logFile.append(input) // log the input
2768-
logFile.append('\n')
2769-
2770-
and then closes the log file to flush any buffered contents to disk:
2771-
2772-
logFile.close()
2773-
2774-
That's it. In "Goodbye" we don't need to do anything for the example. If we had kept a log
2775-
file open to log information across multiple steps, however, we'd put the `logFile.close()`
2776-
into that handler.
2777-
2778-
2779-
2780-
### Why is the type of `pathAndRoundOpt` explicitly specified?
2781-
2782-
Note that in the field declaration
2783-
2784-
var pathAndRoundOpt : Option[(String,Int)] = None
2785-
2786-
we explicitly specify the type as `Option[(String,Int)]` rather than letting the compiler
2787-
infer it for us. The reason this is necessary is that on its own, the compiler would
2788-
(correctly) infer the type by looking at the initialization value and would conclude it
2789-
should be the type of the singleton object `None` (which could be written down as `None.type`).
2790-
2791-
This would later lead to a compile time error when we attempt to assign the updated `Some`
2792-
value, since that type does not match what is expected.
2793-
2794-
To make this work as intended, we need to tell the compiler that what we really want is for the
2795-
field to have the parent type of `None` and `Some`, which - taking the polymorphic type of
2796-
the wrapped value into account - is `Option[(String,Int)]`. Hence the explicit specification
2797-
of the field's type.
2798-
2799-
2800-
2801-
### What does `pathAndRoundOpt.foreach()` do?
2802-
2803-
The objective of that code is to test whether the `Option` value is a `None` or a `Some`
2804-
and, if it is a `Some`, to extract the wrapped value so we can work with it.
2805-
2806-
Let's first look at a "brute force", more procedural implementation of the same code:
2807-
2808-
if(pathAndRoundOpt.isDefined) {
2809-
val pathAndRound = pathAndRoundOpt.get
2810-
/* ... */
2811-
}
2812-
2813-
We can do the same with a `match` expression, which many Scala programmers consider preferable
2814-
compared to `if`:
2815-
2816-
pathAndRoundOpt match {
2817-
case Some(pathAndRound) => /* ... */
2818-
case None => // do nothing
2819-
}
2820-
2821-
But the code in the example illustrates another possibility: use `foreach`.
2822-
As the name implies, `foreach` is a method available on collection types that executes
2823-
a function for each element of the collection. The reason we can use this on `Option`
2824-
is that an `Option` value mimics a collection that is either empty (if the `Option`
2825-
value is `None`) or that contains a single element consisting of the wrapped value (if
2826-
the `Option` is `Some`). So the code we used:
2827-
2828-
pathAndRoundOpt.foreach(pathAndRound => {
2829-
val (dirPath,roundIndex) = pathAndRound
2830-
/* ... */
2831-
})
2832-
2833-
works because the closure `pathAndRound => {/*...*/}` is only executed if the
2834-
`Option` is a `Some`, i.e. if indeed the "Welcome" command was already received
2835-
and the directory path and round index have become available.
2836-
2837-
For the example shown here, the solution feels a bit contrived since there are really no
2838-
savings in terms of code size. However, the approach demonstrates a more general principle
2839-
that is extremely useful and in many situations much more elegant than `if` or `match`,
2840-
not least because it also extends to other collection methods like `map`, where the benefits
2841-
may be more evident. In `map`, we can provide a function that transforms the wrapped value
2842-
into a new value, resulting in an `Option` wrapping that new value without the need to handle
2843-
the `None` case *at all*.
2844-
28452585

28462586

28472587

@@ -2928,83 +2668,12 @@ When you build and publish this bot, you will see that the master bot displays a
29282668
status text bubble saying "Master" next to it.
29292669

29302670

2671+
# You Made It!
29312672

2932-
2933-
2934-
# Bot #12: Handler Object
2935-
2936-
## Objective
2937-
2938-
In preceding bot versions we occasionally held some state inside the Bot object, which contained
2939-
our control function.
2940-
2941-
2942-
## Source Code v2
2943-
2944-
// omitted: class ControlFunctionFactory
2945-
// omitted: class CommandParser
2946-
// omitted: class View
2947-
2948-
class ControlFunction() {
2949-
// this method is called by the server
2950-
def respond(input: String): String = {
2951-
val (opcode, params) = CommandParser(input)
2952-
opcode match {
2953-
case "Welcome" =>
2954-
welcome(
2955-
params("name"),
2956-
params("path"),
2957-
params("apocalypse").toInt,
2958-
params("round").toInt
2959-
)
2960-
case "React" =>
2961-
react(
2962-
View(params("view")),
2963-
params("entity"),
2964-
params
2965-
)
2966-
case "Goodbye" =>
2967-
goodbye(
2968-
params("energy").toInt
2969-
)
2970-
case _ =>
2971-
"" // OK
2972-
}
2973-
}
2974-
2975-
def welcome(name: String, path: String, apocalypse: Int, round: Int) = ""
2976-
2977-
def react(view: View, entity: String, params: Map[String, String]) =
2978-
if( entity == "Master" ) reactAsMaster(view, params)
2979-
else reactAsSlave(view, params)
2980-
2981-
def goodbye(energy: Int) = ""
2982-
2983-
def reactAsMaster(view: View, params: Map[String, String]) = ""
2984-
2985-
def reactAsSlave(view: View, params: Map[String, String]) = ""
2986-
}
2987-
2988-
## What is going on?
2989-
2990-
The setup is quite staright-forward: instead of handling the invocations from the server
2991-
inside the `respond()` method, we parse the command and feed it through a `match`/ `case`
2992-
pattern matcher. The pattern matcher contains one handler for each of the opcodes the
2993-
server may send (we ignore unknown opcodes).
2994-
2995-
Each handler extracts frequently-used parameters from the parsed parameter maps and
2996-
feeds them as arguments to an opcode-specific handler method:
2997-
2998-
* the `welcome()` method handles the `Welcome` opcode
2999-
* the `react()` method handles the `React` opcode
3000-
* the `goodbye()` method handles the `Goodbye` opcode
3001-
3002-
Within the `react()` handler method we inspect the entity for which a response is
3003-
required and then branch into the appropriate entity-specific handler:
3004-
3005-
* the `reactAsMaster()` method handles `React` invocations for the (master) bot
3006-
* the `reactAsSlave()` method handles `React` invocations for mini-bots
3007-
2673+
For the moment, this is where the tutorial ends. If you are interested in advancing further,
2674+
have a look at the various frameworks for Scalatron bots that people have published online.
2675+
You can find some references to them in the [online resources page](http://scalatron.github.com/pages/elsewhere.html)
2676+
of the Scalatron web site.
30082677

30092678

30102679

-4.79 KB
Binary file not shown.
-26.8 KB
Binary file not shown.

0 commit comments

Comments
 (0)