@@ -2582,266 +2582,6 @@ aberrations. The Scalatron protocol provides several mechanisms for doing this:
2582
2582
state parameters which the server maintains for it. These state parameters can be inspected
2583
2583
in the browser-based Scalatron IDE while single-stepping through a sandboxed simulation.
2584
2584
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
-
2845
2585
2846
2586
2847
2587
@@ -2928,83 +2668,12 @@ When you build and publish this bot, you will see that the master bot displays a
2928
2668
status text bubble saying "Master" next to it.
2929
2669
2930
2670
2671
+ # You Made It!
2931
2672
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.
3008
2677
3009
2678
3010
2679
0 commit comments