Search Unity

Robo-Reindeer RUMBLE! — a festive MiniScript programming game

Discussion in 'Works In Progress - Archive' started by JoeStrout, Dec 16, 2015.

  1. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I'm happy to present the first public release of Robo-Reindeer Rumble, a fun little programming game for the holidays.


    This game is based on MiniScript, a new scripting language I'm working on designed to work especially well with Unity. MiniScript features a clean syntax with minimal punctuation; a small yet complete set of types including strings, lists, and maps; and a decent library of intrinsic functions.

    It is also very easy to extend and embed in a Unity app, which is what led to Robo-Reindeer Rumble!

    This is the very first functional release of the game, so options for the deer are pretty limited. They can damage other deer in two ways: ramming into them at least 50% of full speed, or by throwing snowballs with the "throw" function.

    For example, here's a script for a reindeer that turns until it sees an enemy, then charges, full speed ahead!

    Code (MiniScript):
    1. // Name: Charger
    2. while 1
    3.     wait(0.01)
    4.     see = look
    5.     if see != null then
    6.         // see an enemy; don't turn, but run straight ahead
    7.         print("CHARGE!!!")
    8.         speed = 100
    9.     else
    10.         // don't see anything, so let's slow down and turn a bit!
    11.         print
    12.         heading = actual.heading + 5
    13.         speed = 20
    14.     end if
    15. end while
    And here's one that just turns in place until it sees an enemy, and then throws snowballs:

    Code (MiniScript):
    1. // Name: Spinner
    2. print("I spin!")
    3. while 1
    4.     wait(0.01)
    5.     see = look
    6.     if see != null then
    7.         print("Throw at E" + see.energy + " at " + see.distance + "!")
    8.         throw
    9.         heading = heading + rnd * 10 - 5
    10.         wait(0.5)
    11.     else
    12.         // don't see anything, so let's turn a bit!
    13.         heading = actual.heading + 5
    14.     end if
    15. end while
    You can copy this code, switch to the game, click the Edit button by any reindeer, and paste the code right in. Use it as a starting point for your own designs, or start from scratch!

    Let's use this thread as a place to share our reindeer scripts — and when it's not just me any more, I'll start an almost-daily "king of the hill" style tournament. Can you build a smarter robot reindeer?

     
    Last edited: Dec 17, 2015
  2. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I've updated the game with sound effects, a new optional parameter for throw, and a new deerCount variable that returns the number of robo-reindeer still in the arena.

    The parameter for throw specifies how much energy to put into the snowball. This determines how much damage is done to the target if it hits, but it is also subtracted from your own energy store. If your energy goes negative, you are completely frozen until your energy recovers past zero.

    For example, here's a deer that spins in a circle until it sees a deer, and then attempts to one-hit kill it by putting 100 energy into a single throw:

    Code (MiniScript):
    1. // Name: One-Hit Hilda
    2. while 1
    3.     wait(0.01)
    4.     see = look
    5.     if see != null then
    6.         print("Take THAT!")
    7.         throw(100)
    8.     else
    9.         // don't see anything, so let's turn a bit!
    10.         heading = actual.heading + 5
    11.     end if
    12. end while
    If it doesn't miss, this is devastating, as no reindeer can have more than 100 hit points. But it also leaves itself very vulnerable to attack while it recharges. (FYI, robo-reindeer are solar powered.)

    Why not try it yourself? Just copy the above, run the game, Edit any reindeer, and paste it in.

    I can think of two obvious ways to improve this: use the health property of the look result to only put as much energy in the snowball as it should take to destroy the target. (But if the snowball misses the target and is lucky enough to hit someone else, then it may not be enough for that one.) Or, use the new deerCount variable to change your behavior based on how many other deer are around.

    Can anyone make a reindeer script that reliably beats the one above?
     
    Last edited: Dec 17, 2015
  3. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    It's been suggested to me that I need to have some better built-in starter scripts than the "Hello World!" scripts you get now. So, that's on my to-do list... but in the meanwhile, here's another example.

    Code (MiniScript):
    1. // Name: Sprayer
    2. // This reindeer simply spins around rapidly, spewing low-energy
    3. // snowballs in all directions.  Hopefully enough of these will
    4. // hit the other reindeer to wear them down!
    5. print("Whee!!!")
    6. while 1
    7.     heading = actual.heading + 100
    8.     if energy > 5 and rnd < 0.1 then
    9.         throw(5)
    10.     end if
    11.     wait(0.1)
    12. end while
    Also, note that I added an actual map, containing heading and speed. Since the reindeer has a limited acceleration & turning capability, the heading & speed you assign won't be reached right away. So now you can look at actual.heading and actual.speed to find out where what your deer really is doing at any instant.
     
  4. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Here is more information on the extensions to MiniScript which are specific to Robo Reindeer code.


    heading
    This is a global variable which to which you can assign the desired heading (direction) for your reindeer. It is in degrees from 0 to 360; 0 is due East, 45 is Northeast, and so on. (You can assign values outside that range, but they will be remapped to the standard range on the next frame.) Robo-reindeer can't turn instantly, and can't turn at all when they're out of energy, but they will turn (whichever way is shortest) to this heading as soon as they can.

    speed
    This global variable sets your reindeer's target speed, as a percentage of maximum speed. Negative values shift your robo-reindeer into reverse. The valid range for this is -50 to 100. Just as with heading, the reindeer may will achieve this speed as quickly as possible, given a limited acceleration rate and energy. Note that movement consumes energy in proportion to your actual speed, and when the reindeer's energy level is below 5, it will start to slow down (until at an energy level of 0 or less, it can't move at all).

    (heading and speed are the only special variables you should assign to, though of course you can make up your own variables and assign to those as you please.)


    actual
    This is a global variable you should only read, not set. It returns a map containing two elements: heading and speed. These are the actual current heading and speed of your robo-reindeer (as opposed to the global variables above, which are your target heading and speed). So, actual.heading is which way your reindeer is facing, and actual.speed is how fast your reindeer is moving.

    deerCount
    You can tell how many robo-reindeer are still in the arena by this global variable. This count includes yourself.

    energy
    This global variable tells you your robo-reindeer's energy level. Energy is used for almost everything a robo-reindeer does: movement, turning, throwing snowballs, even thinking. Energy slowly increases (robo-reindeer are solar powered) up to a maximum of 100. Energy may go negative (dipping into emergency reserve power), but the reindeer will be frozen in place until its energy level increases past zero.

    health
    Use this global variable to read your reindeer's current hit points. These start at 100, and when they reach zero, the robo-reindeer is scrapped.

    position
    This global variable is a map containing the current x and y position of your reindeer. position.x values go from -50 (left side of the arena) to +50 (right). position.y ranges from -50 (bottom of the arena) to +50 (top).


    look
    This is a function that returns information on the closest reindeer within 5° of your current (actual) heading. If there is no reindeer in that direction, it will return null. Otherwise, the result is a map containing:
    • distance: distance to the reindeer seen (in the same units as position)
    • heading: which way the other reindeer is facing
    • speed: how fast the other reindeer is moving
    • energy: how much energy the other reindeer has
    • health: how many hit points the other reindeer has left
    throw(energy=20)
    This function is used to throw a snowball. You can choose how much energy to put into the snowball; this amount is subtracted from your own energy, and is how many points of health are subtracted from any reindeer hit. If you say throw with no parameters, it throws a snowball with 20 energy. The snowball begins just in front of your reindeer, and travels in the direction of your current (actual) heading.


    These are the only extensions to the standard MiniScript language. You now know everything there is to know about programming robo reindeer... what can you make them do?
     
  5. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    @JoeStrout - The parser doesn't seem to handle Windows' CR-LF line terminators. Any way you could make it so I can paste from Windows?
     
  6. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Doh! I thought I was handling any standard line ending. To be clear, you can paste into the TextField and it looks correct there, but when you check syntax, you get an error?

    And, do you get the same problem when you just type code into the TextField directly, hitting Return after each line? A simple test would be:

    Code (Miniscript):
    1. print(1)
    2. wait
    3. print(2)
    EDIT: OK, I've fired up my Windows box to check it out. It appears that the code works fine when you type it in, but when I paste it in from Notepad, I get a "got Unknown where EOL expected" error.

    (I also find that when I copy a code block from this web page, it not only includes the code, but the line numbers and two extra line breaks per line... at least when using IE. And that has nothing to do with Unity, because I get exactly the same thing pasting the code into Notepad. Seriously, how do people ever get anything done on Windows?)

    I'll dig into that line break problem right now!
     
    Last edited: Dec 19, 2015
  7. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, I've fixed the problem, and @TonyLi, you were right: it was a failure to account for Windows line endings in the lexer.

    Though I still find it odd that the InputField doesn't normalize line endings in any way (you apparently can get different results typing them vs. pasting them on Windows), but MiniScript (and thus RoboReindeer) now deals with any combination of line endings you may throw at it.

    I look forward to seeing what you come up with!
     
  8. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Thanks, Joe! I'll make a little time tomorrow to get something written and posted!
     
    JoeStrout likes this.
  9. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I've updated the game with a big new feature: Meadow Mines! These are little piles of pure destruction. Every point of energy you put into them does two points of damage to any reindeer that steps on it! Simple call drop(energy, dely) to drop a meadow mine with the given energy and arming delay.

    Here you can see the green reindeer has laid a couple of mines. The one on the right is still fresh (not yet armed). The one on the left is armed and dangerous.

    ...And, here's the same reindeer a moment later, running over his own (armed) meadow mine.

    So yes, you can step on your own mines; once they're out they don't care where they came from.

    Incidentally, I've also updated the default script that reindeer start with for a new player. The starter script is now this:

    Code (MiniScript):
    1. print("Edit me!")
    2. while 1
    3.     heading = rnd * 360
    4.     speed = 50 + rnd * 50
    5.     wait(rnd * 2)
    6.     speed = 0
    7.     wait(rnd)
    8.     if rnd < 0.1 then
    9.         throw(20)  // throw a snowball with 20 energy
    10.     else if rnd < 0.2 then
    11.         drop(10, 2) // drop mine with 10 energy and 2-second delay
    12.     end if
    13. end while
    This makes a reindeer that wanders around a bit aimlessly, occasionally dropping mines or throwing snowballs (again, aimlessly). It's a much more interesting start than the previous Hello World script, and demonstrates some of the features you will probably want to include in your own robo-reindeer brains. (Note that if you've played before, you will probably still have the old Hello World scripts stored in your player prefs. Just copy/paste the above, or flush browser data.)

    So now there are three ways to attack your opponents: throwing snowballs, dropping mines, and good old-fashioned bashing into them. Why not give it a try?
     
  10. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Okay, here's a challenger:

    Code (MiniScript):
    1. // Blitzen:
    2.  
    3. min = function(a,b)
    4.     if (a < b) then
    5.        return a
    6.     else
    7.        return b
    8.     end if
    9. end function
    10.  
    11. ticksLeft = 0
    12. headingDelta = 5
    13. wanderSpeed = 30
    14. while 1
    15.     wait(0.01)
    16.     see = look
    17.     if (see != null) then
    18.         speed = 100
    19.         print("Moo!")
    20.         if ((see.distance < 5) and (energy > 30)) then
    21.             throwEnergy = min((energy - 20), see.health)
    22.             print("Blitz!")
    23.            throw(throwEnergy)
    24.         end if
    25.     else
    26.         ticksLeft = ticksLeft - 1
    27.         if (ticksLeft <= 0) then
    28.             ticksLeft = 100 * rnd
    29.            if (rnd < 0.5) then
    30.                 headingDelta = -5
    31.             else
    32.                 headingDelta = 5
    33.             end if
    34.             wanderSpeed = 30
    35.         end if
    36.         heading = actual.heading + headingDelta
    37.         if (wanderSpeed > 20) then
    38.             wanderSpeed = wanderSpeed - 0.5
    39.         end if
    40.         speed = wanderSpeed
    41.     end if
    42. end while
    He seems to do more harm than good with meadow mines, so I left them out.

    Is there a way to detect mines?
     
    JoeStrout likes this.
  11. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    @TonyLi, that's great! Your Blitzen seems like a strong contender indeed!

    No, there is no way to detect mines (or snowballs, for that matter). Safe use of them would rely on moving in a way that ensures you don't trip over your own mines. So, yeah, the way Blitzen dashes around the field, he's better off without them.

    One note on MiniScript style: parentheses aren't needed around the condition in an "if" (or "while") statement, and operator precedence follows the standard convention. So "if ((see.distance < 5) and (energy > 30)) then" could be written as just "if see.distance < 5 and energy > 30 then". Getting rid of unnecessary punctuation was one of my design goals for the language... though of course, I can't stop you from throwing in extra parens if you insist! :)

    P.S. Also, your ticksLeft works fine for counting loop iterations if you like, but also note the time intrinsic function, which returns the elapsed time since the start of the script. It might be handy in cases like this.
     
  12. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    @JoeStrout - Well, then you really won't like this code. ;) I wanted to do a quick FSM model, but I didn't take the time to design code for re-use so there's a lot of duplication. But since that's all the time I have for today, so be it.

    Also, this raises an error:
    Code (MiniScript):
    1. if see != null and (see.distance < 10 or didFullSweep == 1) then
    Presumably because MiniScript doesn't do short-circuit evaluation?

    Code (MiniScript):
    1. // Corner lurker:
    2. min = function(a,b)
    3.     if (a < b) then
    4.        return a
    5.     else
    6.        return b
    7.     end if
    8. end function
    9.  
    10. GotoLowerLeft = function()
    11.     print("Going to lower left")
    12.     globals.heading = 225
    13.     globals.speed = 100
    14.     position = globals.position
    15.     while ((position.x > -49) or (position.y > -49))
    16.         wait(0.1)
    17.         if ((globals.energy > 30) and (position.x > -40)) then
    18.             drop(2, 1)
    19.         end if
    20.     end while
    21.     return @ScanInLowerLeft
    22. end function
    23.  
    24. GotoUpperLeft = function()
    25.     print("Going to upper left")
    26.     globals.heading = 135
    27.     globals.speed = 100
    28.     position = globals.position
    29.     while ((position.x > -49) or (position.y < 49))
    30.         wait(0.1)
    31.         if ((globals.energy > 30) and (position.x > -40)) then
    32.             drop(2, 1)
    33.         end if
    34.     end while
    35.     return @ScanInUpperLeft
    36. end function
    37.  
    38. ScanInLowerLeft = function()
    39.     print("Scanning from lower left")
    40.     globals.speed = 0
    41.     actual = globals.actual
    42.     headingDelta = 5
    43.     didFullSweep = 0
    44.     myHealth = globals.health
    45.     while 1
    46.         if (globals.health < myHealth) then
    47.             return @GotoUpperLeft
    48.         end if
    49.         wait(0.01)
    50.         see = look
    51.         hasTarget = (see != null)
    52.         if hasTarget then
    53.             isClose = (see.distance < 10)
    54.             hasTarget = isClose or didFullSweep
    55.         end if
    56.         if (hasTarget) then
    57.             throwEnergy = see.health
    58.             if (didFullSweep) then
    59.                 throwEnergy = 5
    60.             end if
    61.             throw(throwEnergy)
    62.             didFullSweep = 0
    63.         else
    64.             globals.heading = actual.heading + headingDelta
    65.             if (actual.heading <= 0) then
    66.                 globals.heading = 5
    67.                 headingDelta = 5  
    68.                 didFullSweep = 1
    69.             else if (actual.heading > 90) then
    70.                 globals.heading = 85
    71.                 headingDelta = -5
    72.             end if
    73.         end if
    74.     end while
    75. end function
    76.  
    77. ScanInUpperLeft = function()
    78.     print("Scanning from upper left")
    79.     globals.speed = 0
    80.     actual = globals.actual
    81.     headingDelta = 5
    82.     didFullSweep = 0
    83.     myHealth = globals.health
    84.     while 1
    85.         if (globals.health < myHealth) then
    86.             return @GotoLowerLeft
    87.         end if
    88.         wait(0.01)
    89.         see = look
    90.         hasTarget = (see != null)
    91.         if hasTarget then
    92.             isClose = (see.distance < 10)
    93.             hasTarget = isClose or didFullSweep
    94.         end if
    95.         if (hasTarget) then
    96.             throwEnergy = see.health
    97.             if (didFullSweep) then
    98.                 throwEnergy = 5
    99.             end if
    100.             throw(throwEnergy)
    101.             didFullSweep = 0
    102.         else
    103.             globals.heading = actual.heading + headingDelta
    104.             if ((actual.heading == 0) or (actual.heading == 360)) then
    105.                 globals.heading = 355
    106.                 headingDelta = -5  
    107.                 didFullSweep = 1
    108.             else if (actual.heading < 270) then
    109.                 globals.heading = 275
    110.                 headingDelta = 5
    111.             end if
    112.         end if
    113.     end while
    114. end function
    115.  
    116. // Main:
    117. state = null
    118. while 1
    119.     if (state == null) then
    120.         state = @GotoLowerLeft
    121.     end if
    122.     state = state
    123. end while
     
  13. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    @TonyLi, you're right, MiniScript doesn't do short-circuit evaluation yet. It's on the to-do list, but for now you'd have to nest the if's. (EDIT: Now done.)

    I dig your Corner Lurker, though. In my test he actually defeated Blitzen, though for a while there I think it could have gone either way.



    All right, people. The gauntlet is thrown. TonyLi is going to be the champion of the holiday Rumble if nobody can knock Corner Lurker and Blitzen off the pedestal. Let's see what you've got!
     
    Last edited: Dec 22, 2015
  14. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    Huh, that's surprising. I ran a few rounds, and Corner Lurker fairly consistently got crushed by your Charger. Blitzen can beat Charger maybe 2 out of 3 times.

    Any other takers? I wonder how a reindeer would fare that just drops mines in a grid pattern from left to right over the whole field.
     
  15. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Well, to be fair, I hadn't included Charger in that particular rumble.

    Tomorrow or Wednesday I'll run a proper set of matches and see who comes out on top, including all the bots I've posted here as well as yours and any others that come up by then.

    Indeed. They'd have to be fairly low-power mines, but that doesn't matter if you bump into enough of them. I thought your Corner Lurker diagonal mine-laying was rather clever.

    I also wonder about a reindeer that hides in a corner, and walls itself in with a series of high-energy mines. It'd be no defense against snowballs, of course, but could be really effective against chargers.
     
  16. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I've added short-circuit evaluation of and and or to MiniScript. So it is now safe to say things like "if see != null and see.distance < 10...". (And there is a whole new set of integration tests to prove it.) Thanks to @TonyLi for pushing this one to the top of the to-do list!

    The game itself has also received a little enhancement where, if your reindeer generates an error at runtime, you can see the full text of that error at the bottom of the script editor the next time you open that up.
     
    TonyLi likes this.
  17. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Hi @JoeStrout, I was messing around a little bit this morning and couldn't figure out how to make good use of the map concatenation operator. I figured since we can add arbitrary key/values there would be a way to iterate over them but I can't seem to figure it out!

    It would be nice if the for loop could operate on map kind of like a Dictionary
    Code (MiniScript):
    1. map = {"hello":2, "world":3}
    2. for word in map
    3.   for i in range(1, word.value, 1)
    4.     print(word.key)
    5.   end for
    6. end for
    Code (output):
    1. hello
    2. hello
    3. world
    4. world
    5. world
    Also, I noticed the script editor doesn't seem to complain about missing "end for" statements. That one tripped me up for a bit.
     
    JoeStrout likes this.
  18. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Hi @eisenpony, thanks for trying it out! I'm not sure if you're saying you're unclear on how the map concatenation operator works — probably not, but just in case: map1 + map2 gives you a new map with all the key/value pairs of map2 added to map1. It's probably not something you will need very often, and is included just for completeness.

    And you're right, there is currently no way to iterate over a map. I see what you're suggesting: the "for...in" operator should give back little maps (mini-maps!) containing value and key. Makes perfect sense. I'll put that on the to-do list!

    Hmm, so it doesn't! That surprises me a bit, but I guess it's simply figuring there's more input coming... and if you insist on running anyway, it will run until it reaches that point, and just stop.

    So I will need to add something for the case where we claim to have given it a complete script, and if there are any unclosed block openers at that point, we throw an error.

    Thanks for these excellent points!
     
  19. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,697
    @JoeStrout - It would be really helpful if you could report the line number of errors.
     
  20. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Tony, you're scaring me.. Give the rest of us a chance!
     
  21. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yes, that's true. That's also on the to-do list. Requires some new plumbing, but I'll get it done.
     
  22. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    All my efforts at intelligence and this silly script I came up with (ripped off) in 5 minutes can't be beat!
    Code (csharp):
    1. // Name: BatteringRam
    2. while 1
    3.     wait(0.01)
    4.     see = look
    5.     if see != null then
    6.         print("RAM U")
    7.         if see.distance > 5 then       
    8.             speed = 100
    9.         else
    10.             speed = -100
    11.         end if
    12.     else
    13.         print()
    14.         heading = actual.heading + 5 * sign(time % 10 - 5)
    15.         speed = 20
    16.     end if
    17. end while
    (gotta love exploits)
     
    Last edited: Dec 22, 2015
    TonyLi and JoeStrout like this.
  23. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I don't see any exploit there. I do see a very wicked robo-reindeer, though!

    In my quick test, sure enough, BatteringRam won 3 out of 3. Blitzen was second in each case, and managed to do some damage, but ultimately fell to BatteringRam's multiple attacks.

    There was one event where poor Corner Lurker got attacked by BatteringRam and Blitzen at the same time. I'm pretty sure it was a thin smear of motor oil when they left the corner.

    Hmm. Charge attacks are extremely effective because they don't take much energy, unlike snowballs and mines, which are both pretty costly. But there must be some defense. I'm tempted to try making a reindeer that detects when it gets hurt (by just watching its own health) and, when that happens, drops a big mine and flees. (Hey, I think that happens in the animal world too... but the packages dropped don't usually explode.) That should deter chargers, I would think.

    But for the moment, at least, I proclaim @eisenpony the new champion!
     
    eisenpony likes this.
  24. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Yes, I was finding Charger extremely difficult to best, so I started thinking of ways to improve him.

    I called it an exploit because the algorithm exploits that, while changing bearing via heading takes time, changing bearing via speed does not.
     
  25. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Not quite true (unless I misunderstand you). When you set the speed to -50 or more (it's limited to that), the reindeer's bearing doesn't change; it just goes in reverse. And it has a limited acceleration rate (currently 100 units/sec^2). So, that does take a little time. But your clever solution proves that this back-up-and-ram-again approach is very effective!
     
  26. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    I've updated MiniScript (and Robo-Reindeer Rumble) with the following new features:
    • You can now use "for ... in" to iterate over maps. The iterator variable in this case gets a key/value pair (with thanks to @eisenpony for suggesting it). For example:
    Code (MiniScript):
    1. d = {"one":1, "two":2, "three":3}
    2. for i in d
    3.     print(i.key + " maps to " + i.value)
    4. end for
    which outputs:
    • You now get an error if you have an open if, while, etc. at the end of the script.
    • Error reports now include the line number. (I don't yet have it highlighting the offending line, nor line numbers next to the text editor, so you'll have to just count from the top... but it's better than nothing.)
     
    eisenpony likes this.
  27. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Okay, perhaps I'm just misusing the term. Anyways, what was important to me was the ability to reset the damage trigger as quickly as possible.

    I stand corrected then, it just seemed like the speed was changing instantaneously.
     
  28. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Yeah, your clever script did make me wonder if I should reduce the acceleration. Certainly it's able to hit repeatedly much faster than I expected! But I'm going to leave it alone for now. Nobody likes a sluggish robot reindeer.

    And I don't believe that BatteringRam will forever remain the champion... somebody will come up with a strategy to defeat it, sooner or later!
     
  29. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    @JoeStrout, is there a way to pass the reindeer object as a parameter? I tried setting the speed and heading from another object and nothing happened. I assume it's because the reindeer members are no longer in scope.

    Code (MiniScript):
    1. SearchBehaviour = { }
    2. SearchBehaviour.act = function()
    3.     speed = rnd * 100
    4.     heading = rnd * 360
    5. end function
    6.  
    7. behaviour = new SearchBehaviour
    8. while 1
    9.     behaviour.act()
    10.     wait(.1)
    11. end while
    This didn't work either
    Code (MiniScript):
    1. SearchBehaviour = { }
    2. SearchBehaviour.act = function(deer)
    3.     deer.speed = rnd * 100
    4.     deer.heading = rnd * 360
    5. end function
    6.  
    7. behaviour = new SearchBehaviour
    8. while 1
    9.     behaviour.act(self)
    10.     wait(.1)
    11. end while
     
  30. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    There is no reindeer object; speed and heading are globals. They're accessible from within methods, but assignment always creates (or updates) a local variable unless you explicitly say otherwise using "globals.". See @TonyLi's Corner Lurker script above for an example, or, here's a working version of your test script:

    Code (MiniScript):
    1. SearchBehaviour = { }
    2. SearchBehaviour.act = function()
    3.     globals.speed = rnd * 100
    4.     globals.heading = rnd * 360
    5. end function
    6.  
    7. behaviour = new SearchBehaviour
    8. while 1
    9.     behaviour.act
    10.     wait(.1)
    11. end while
    Note that when reading values, you don't need the globals prefix for anything not shadowed by a local variable. It's only on assignment that the global prefix is required. (This is different from Lua, where every assignment creates/updates a global variable unless you remember to say otherwise.)
     
  31. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK! I'm finally getting around to running a proper King-of-the-Hill battle. What I'm going to do today is run the same set of reindeer 10 times, and report the results... I think it will be clear which is the champion!

    The Contestants

    The competitors are shown below. The first three are mine; BatteringRam is by @eisenpony; and Corner Lurker and Blitzen are by @TonyLi.

    Code (MiniScript):
    1. // Name: One-Hit Hilda
    2. while 1
    3.    wait(0.01)
    4.    see = look
    5.    if see != null then
    6.      print("Take THAT!")
    7.      throw(100)
    8.    else
    9.      // don't see anything, so let's turn a bit!
    10.      heading = actual.heading + 5
    11.    end if
    12. end while

    Code (MiniScript):
    1. // Name: Spinner
    2. while 1
    3.    wait(0.01)
    4.    enemy = look
    5.    if enemy != null then
    6.      print("Enemy " + enemy.distance + " units away!")
    7.      // If it's very close, hit it HARD, as we are in danger.
    8.      // Further away, be more modest, as we're likely to miss.
    9.      if enemy.distance < 10 then
    10.        throw(enemy.health)
    11.      else if enemy.distance < 40 then
    12.        throw(enemy.health/4)
    13.      else
    14.        throw(energy-10)
    15.      end if
    16.      heading = actual.heading - 5
    17.      speed = -50
    18.      wait(0.5)
    19.      speed = 0
    20.    else
    21.      // don't see anything, so let's turn a bit!
    22.      heading = actual.heading + 10
    23.    end if
    24. end while

    Code (MiniScript):
    1. // Name: Charger
    2. turn = 5
    3. while 1
    4.    wait(0.01)
    5.    see = look
    6.    if see != null then
    7.      // see an enemy; don't turn, but run straight ahead
    8.      print("CHARGE!!!")
    9.      speed = 100
    10.    else
    11.      // don't see anything, so let's slow down and turn a bit!
    12.      print
    13.      heading = actual.heading + turn
    14.      if rnd < 0.01 then; turn = -turn; end if
    15.      speed = 20
    16.    end if
    17. end while

    Code (MiniScript):
    1. // Name: BatteringRam
    2. // Author: @eisenpony
    3. while 1
    4.   wait(0.01)
    5.   see = look
    6.   if see != null then
    7.   print("RAM U")
    8.   if see.distance > 5 then  
    9.   speed = 100
    10.   else
    11.   speed = -100
    12.   end if
    13.   else
    14.   print()
    15.   heading = actual.heading + 5 * sign(time % 10 - 5)
    16.   speed = 20
    17.   end if
    18. end while

    Code (MiniScript):
    1. // Name: Corner lurker
    2. // Author: @TonyLi
    3. min = function(a,b)
    4.    if (a < b) then
    5.     return a
    6.    else
    7.     return b
    8.    end if
    9. end function
    10. GotoLowerLeft = function()
    11.    print("Going to lower left")
    12.    globals.heading = 225
    13.    globals.speed = 100
    14.    position = globals.position
    15.    while ((position.x > -49) or (position.y > -49))
    16.      wait(0.1)
    17.      if ((globals.energy > 30) and (position.x > -40)) then
    18.        drop(2, 1)
    19.      end if
    20.    end while
    21.    return @ScanInLowerLeft
    22. end function
    23. GotoUpperLeft = function()
    24.    print("Going to upper left")
    25.    globals.heading = 135
    26.    globals.speed = 100
    27.    position = globals.position
    28.    while ((position.x > -49) or (position.y < 49))
    29.      wait(0.1)
    30.      if ((globals.energy > 30) and (position.x > -40)) then
    31.        drop(2, 1)
    32.      end if
    33.    end while
    34.    return @ScanInUpperLeft
    35. end function
    36. ScanInLowerLeft = function()
    37.    print("Scanning from lower left")
    38.    globals.speed = 0
    39.    actual = globals.actual
    40.    headingDelta = 5
    41.    didFullSweep = 0
    42.    myHealth = globals.health
    43.    while 1
    44.      if (globals.health < myHealth) then
    45.        return @GotoUpperLeft
    46.      end if
    47.      wait(0.01)
    48.      see = look
    49.      hasTarget = (see != null)
    50.      if hasTarget then
    51.        isClose = (see.distance < 10)
    52.        hasTarget = isClose or didFullSweep
    53.      end if
    54.      if (hasTarget) then
    55.        throwEnergy = see.health
    56.        if (didFullSweep) then
    57.          throwEnergy = 5
    58.        end if
    59.        throw(throwEnergy)
    60.        didFullSweep = 0
    61.      else
    62.        globals.heading = actual.heading + headingDelta
    63.        if (actual.heading <= 0) then
    64.          globals.heading = 5
    65.          headingDelta = 5  
    66.          didFullSweep = 1
    67.        else if (actual.heading > 90) then
    68.          globals.heading = 85
    69.          headingDelta = -5
    70.        end if
    71.      end if
    72.    end while
    73. end function
    74. ScanInUpperLeft = function()
    75.    print("Scanning from upper left")
    76.    globals.speed = 0
    77.    actual = globals.actual
    78.    headingDelta = 5
    79.    didFullSweep = 0
    80.    myHealth = globals.health
    81.    while 1
    82.      if (globals.health < myHealth) then
    83.        return @GotoLowerLeft
    84.      end if
    85.      wait(0.01)
    86.      see = look
    87.      hasTarget = (see != null)
    88.      if hasTarget then
    89.        isClose = (see.distance < 10)
    90.        hasTarget = isClose or didFullSweep
    91.      end if
    92.      if (hasTarget) then
    93.        throwEnergy = see.health
    94.        if (didFullSweep) then
    95.          throwEnergy = 5
    96.        end if
    97.        throw(throwEnergy)
    98.        didFullSweep = 0
    99.      else
    100.        globals.heading = actual.heading + headingDelta
    101.        if ((actual.heading == 0) or (actual.heading == 360)) then
    102.          globals.heading = 355
    103.          headingDelta = -5  
    104.          didFullSweep = 1
    105.        else if (actual.heading < 270) then
    106.          globals.heading = 275
    107.          headingDelta = 5
    108.        end if
    109.      end if
    110.    end while
    111. end function
    112. // Main:
    113. state = null
    114. while 1
    115.    if (state == null) then
    116.      state = @GotoLowerLeft
    117.    end if
    118.    state = state
    119. end while

    Code (MiniScript):
    1. // Name: Blitzen
    2. // Author: @TonyLi
    3. min = function(a,b)
    4.    if (a < b) then
    5.     return a
    6.    else
    7.     return b
    8.    end if
    9. end function
    10. ticksLeft = 0
    11. headingDelta = 5
    12. wanderSpeed = 30
    13. while 1
    14.    wait(0.01)
    15.    see = look
    16.    if (see != null) then
    17.      speed = 100
    18.      print("Moo!")
    19.      if ((see.distance < 5) and (energy > 30)) then
    20.        throwEnergy = min((energy - 20), see.health)
    21.        print("Blitz!")
    22.       throw(throwEnergy)
    23.      end if
    24.    else
    25.      ticksLeft = ticksLeft - 1
    26.      if (ticksLeft <= 0) then
    27.        ticksLeft = 100 * rnd
    28.       if (rnd < 0.5) then
    29.          headingDelta = -5
    30.        else
    31.          headingDelta = 5
    32.        end if
    33.        wanderSpeed = 30
    34.      end if
    35.      heading = actual.heading + headingDelta
    36.      if (wanderSpeed > 20) then
    37.        wanderSpeed = wanderSpeed - 0.5
    38.      end if
    39.      speed = wanderSpeed
    40.    end if
    41. end while

    The Results

    Match 1: Corner Lurker's mines dealt widespread damage, and caused the destruction of Charger. But BatteringRam quickly took out all the other reindeer, including Corner Lurker. Winner: BatteringRam.

    Match 2: BatteringRam again quickly destroyed most of the other reindeer, except Charger. These two circled for position for a while, but in the end, Charger got in the lethal blow. Winner: Charger.

    Match 3: In this match, Corner Lurker managed to destroy BatteringRam with several well-placed snowballs, though not before BatteringRam took out One-Hit Hilda. Charger mopped up the rest, including Corner Lurker. Winner: Charger.

    Match 4: Corner Lurker destroyed Charger this time, but was taken out by BatteringRam, which also destroyed all the other reindeer. Winner.: BatteringRam. (BatteringRam and Charger are 2-2 at this point.)

    Match 5: Our most exciting match yet! One-Hit Hilda took out Blitzen, but was then destroyed by Spinner, who was killed by Charger. Corner Lurker left mines which tripped up BatteringRam, and then finished it off with a snowball. That left Charger and Corner Lurker, and Charger emerged victorious. Winner: Charger.

    Match 6: Another exciting match; One-Hit Hilda destroyed Charger almost immediately. Spinner's snowballs and Corner Lurker's mines did quick damage early on, but not enough to destroy anyone. BatteringRam took out One-Hit Hilda, Spinner, and Blitzen, leaving a show-down with Corner Lurker. On its final approach, Corner Lurker got in a good hit and finished BatteringRam off. Winner: Corner Lurker.

    Match 7: One-Hit Hilda quickly took out BatteringRam, but was itself destroyed by Spinner. Charger then methodically destroyed the remaining robo-reindeer. Winner: Charger.

    Match 8: This one came down to BatteringRam and Charger circling each other in the corner, where they had both attacked Corner Lurker. This was near the mines, which they each tripped over as they jockeyed for position. In the end, BatteringRam destroyed Charger, but then itself was almost immediately destroyed by one of Corner Lurker's mines. I'm calling this one a draw!

    Match 9: Wow! Corner Lurker took out both Charger and BatteringRam with its snowballs (helped by them stepping on some mines). One-Hit Hilda took out Spinner. At this point the only remaining reindeer were One-Hit Hilda, Corner Lurker, and Blitzen... and all of them were out of energy! Blitzen recovered first, and took out One-Hit Hilda. This left it exhausted again, but it recovered before Corner Lurker again (which had gone deep into the red taking out the two heavy hitters). Blitzen turned to Corner Lurker, made its approach run, threw a snowball from point-blank range, and ...missed. Corner Lurker finally recovered, found Blitzen in its face, and destroyed it with a snowball. Winner: Corner Lurker.

    Match 10: One-Hit Hilda killed Spinner, then was killed by BatteringRam. Corner Lurker, pitching from the corner, again took out both Charger and BatteringRam. Again it came down to Corner Lurker and Blitzen, both exhausted, but this time, right on top of each other. Blitzen woke up first — several times — but because it was right on top of its target, it was unable to hit. Corner Lurker, when it finally woke up, had the same problem. These two are hugging more than fighting now. Another draw.

    Results Summary
    Charger: 4 wins
    BatteringRam: 2 wins
    Corner Lurker: 2 wins


    And so it is with some embarrassment that I am forced to announce that the current Robo-Reindeer Rumble champion is... Charger, intended as an example program by yours truly. :p BatteringRam and CornerLurker were both extremely strong, but there were enough other strong contenders in this match that random elements (mainly initial position and facing) kept any one strategy from dominating.

    I do not by any means imagine that Charger is unbeatable, or that the best possible Robo-Reindeer strategies have been found. Watching these programs, I think it's clear that there is plenty of room for improvement. I look forward to Charger getting knocked firmly off the hill!
     
    eisenpony and TonyLi like this.
  32. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Hey JoeStrout, I pointed a colleague to this thread and he was interested in using robo-reindeer for a mini programming competition. It's just a few of us in the office, but just wanted to clear it with you and confirm the game will be accessible for us on Monday.

    We won't have much time for the game but if anything interesting falls out of it, I'll post back here!

    Btw, I noticed last week that the global "energy" variable was not working as expected. It seemed to always return 50 regardless of the actual value.
     
  33. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    Absolutely, I'll leave the game up indefinitely, certainly through next week. I'm tickled pink that somebody wants to use it!

    Oops, that's no good. I'll look into it. Thanks for the report!
     
  34. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    OK, it appears that I recently broke the way global variables are set from Unity code. It was affecting other globals like health, too (but not things that were tucked inside a map, like position or actual). Fixed now... very sorry for the confusion!