Viliam Búr

2008/08/10

Water Battle Map for Wesnoth, Improved

In previous two articles we have designed a multiplayer scenario for water battle, called "2p - Pool", where one player can recruit only Merfolk units, and other player can recruit only Naga and Saurian units; the map was published on Wesnoth add-on server. In the last article we will move the choice between Merfolk faction and Naga faction inside the scenario. Each player will decide by moving his/her leader into one of two available keeps; this will allow also Merfolk vs Merfolk and Naga vs Naga combats.

Please note: A more "natural" way of choosing Merfolk faction or Naga faction would be to create two multiplayer factions, and then to create a multiplayer era containing these two factions. When starting a multiplayer game, the host could choose this (or any other) era for playing this (or any other) scenario. But we will do it differently, because we want to use these factions only for this scenario, and because this is a scenario logic tutorial, not an era tutorial.

Scenario logic is implemented using events. Events are commands in form: "when this happens, do that". On Wesnoth wiki pages you can find a list of possible commands, in this article only the ones used will be explained.

Let's start by putting labels on the keeps, saying "this is a Merfolk keep" and "this is a Naga keep". We will use the scenario file "2p_Pool.cfg" created in the previous article, and add events to the end of the file.

[multiplayer]

 # ...here is what we wrote in the previous article...

 [event]
  name=prestart
  [label]
   x,y=4,5
   text=_"Merfolk"
  [/label]
  [label]
   x,y=4,9
   text=_"Nagas"
  [/label]
  [label]
   x,y=30,5
   text=_"Merfolk"
  [/label]
  [label]
   x,y=30,9
   text=_"Nagas"
  [/label]
 [/event]

[/multiplayer]

The "[event]" and "[/event]" marks mean "this is the beginning / the end of event description". The "name" specifies when should this event happen; value "prestart" means "before the start of scenario, before anything is displayed". This is the right moment to do last-minute map changes. The remaining marks are the commands executed when the event is triggered, that is before the scenario starts.

The "[label]" and "[/label]" marks define command "put a text label on a hex on map". The "x" and "y" are coordinates of the hex. (The coordinates are displayed at the top right corner of screen, both in map editor and in game.) The "text" is a text of label. The text is preceded by "_" symbols to allow translating it to other languages later.

This is the most simple way to write the script. But it may not be so easy to maintain, especially when the script becomes longer. It is not obvious that "4,5" are the coordinates of player 1 Merfolk keep; and if map designer would choose to move the keep to another place, it would not be obvious which parts of the script should be updated. This can be solved by creating a macro called "KEEP_P1M", which will represent the location of player 1 Merfolk keep, but can be defined in one place and used repeatedly. Macros can be defined in any part of the script before their first use, so we can put this macro before the events, or at the beginning of the file. The version with macros will look like this:

#define KEEP_P1M
x,y=4,5
#enddef

#define KEEP_P1N
x,y=4,9
#enddef

#define KEEP_P2M
x,y=30,5
#enddef

#define KEEP_P2N
x,y=30,9
#enddef

[multiplayer]

 # ...here is what we wrote in the previous article...

 [event]
  name=prestart
  [label]
   {KEEP_P1M}
   text=_"Merfolk"
  [/label]
  [label]
   {KEEP_P1N}
   text=_"Nagas"
  [/label]
  [label]
   {KEEP_P2M}
   text=_"Merfolk"
  [/label]
  [label]
   {KEEP_P2N}
   text=_"Nagas"
  [/label]
 [/event]

[/multiplayer]

Scenario start screenshot

The next event will be the selection of the keep. Let's write the code for player 1 selecting the Merfolk keep. When a unit belonging to player 1 goes to the player 1 Merfolk keep, player 1 will be allowed to recruit Merfolk units, and the player 1 Naga keep will be destroyed.

[multiplayer]

 # ...here is what we wrote previously...

 [event]
  name=moveto
  [filter]
   side=1
   x,y=4,5
   [filter_location] 
    terrain=Khw
   [/filter_location]
  [/filter]
  first_time_only=yes

  [set_recruit]
   side=1
   recruit=Merman Fighter,Merman Hunter,Mermaid Initiate
  [/set_recruit]
  [label]
   x,y=4,9
   text=""
  [/label]
  [terrain]
   x= 3, 3, 4, 4, 4, 5, 5
   y= 9,10, 8, 9,10, 9,10
   terrain=Wo
  [/terrain]
  [terrain]
   x,y=4,7
   terrain=Ww
  [/terrain]
 [/event]

[/multiplayer]

The event type "moveto" is triggered after a unit is moved into a situation described by "[filter]". (The unit must end its move there, not only move through.) Because such situation can possibly happen many times, the "first_time_only" specifies if the event is triggered only in first such situation, or in each such situation. We want to select the keep only once.

The "[filter]" and "[/filter]" marks specify the situation when the event is ready. The "side" means that is must be after move of unit belonging to player 1. The "x,y" means that the unit must have moved into hex 4,5 (the player 1 Mefolk keep). The "[filter_location]" specifies further conditions for the terrain where the unit ended its move; it must be a "Khw" terrain, which is code for a water keep. (Why do we check the presence of the keep after we wrote that the hex must be 4,5? This is a trick to prevent selecting already destroyed keep. If player 1 selects the Naga keep, the Merfolk keep will be destroyed; if later a unit goes to hex 4,5, there will be no keep terrain, so this event will not be triggered.)

The "[set_recruit]" and "[/set_recruit]" marks define command to change the recruitment list; player 1 will be able to recruit only Merfolk units. The "[label]" and "[/label]" marks put a text label on the map; it this situation they rewrite already existing label ("Nagas" at player 1 Naga keep) to an empty string, this means removing the label.

The "[terrain]" and "[/terrain]" marks define command to change map terrain; the "x,y" is the position of hex which should be changed, and the "terrain" is the code of the new terrain. It is also possible to specify multiple positions by providing a list of x-coordinates and a list of corresponsing y-coordinates; so the changed hexes in this example are 3,9, 3,10, 4,8, 4,9, 4,10, 5,9, and 5,10. The terrain code "Wo" means deep water; we will sink the other castle deeply. The terrain code "Ww" means shallow water; we will just remove the bridge at the starting location.

Now again, this piece of script would be easier to maintain using some macros:

[multiplayer]

 # ...here is what we wrote previously...

 [event]
  name=moveto
  [filter]
   side=1
   {KEEP_P1M}
   [filter_location] 
    terrain={TERRAIN_KEEP}
   [/filter_location]
  [/filter]
  first_time_only=yes

  [set_recruit]
   side=1
   recruit={UNITS_MERFOLK}
  [/set_recruit]
  [label]
   {KEEP_P1N}
   text=""
  [/label]
  [terrain]
   {CASTLE_P1N}
   terrain={TERRAIN_WATER_DEEP}
  [/terrain]
  [terrain]
   {START_P1}
   terrain={TERRAIN_WATER}
  [/terrain]
 [/event]

[/multiplayer]

We need 4 such events: player 1 selecting Merfolk keep, player 1 selecting Naga keep, player 2 selecting Merfok keep, and player 2 selecting Naga keep. We could make 4 copies of this event and make the necessary changes. Or we could change it to macro (called SELECT_FACTION) with parameters. Macros can be written once and used repeatedly. The parameters are the parts of macro which are changing.

In this example the changing parts of macro are: the number of player, 1 or 2 (let's call it SIDE); the coordinates of the selected keep (let's call it KEEP); the list of recruitable units (UNITS); the coordinates of the other keep for removing the label (OTHER_KEEP); the coordinates of the other castle and keep to be sunk (OTHER_CASTLE); and the starting location of the player (START). So the macro version of this event will look like this:

#define SELECT_FACTION SIDE KEEP UNITS OTHER_KEEP OTHER_CASTLE START
 [event]
  name=moveto
  [filter]
   side={SIDE}
   {KEEP}
   [filter_location] 
    terrain={TERRAIN_KEEP}
   [/filter_location]
  [/filter]
  first_time_only=yes

  [set_recruit]
   side={SIDE}
   recruit={UNITS}
  [/set_recruit]
  [label]
   {OTHER_KEEP}
   text=""
  [/label]
  [terrain]
   {OTHER_CASTLE}
   terrain={TERRAIN_WATER_DEEP}
  [/terrain]
  [terrain]
   {START}
   terrain={TERRAIN_WATER}
  [/terrain]
 [/event]
#enddef

And we will call it like this:

#define UNITS_MERFOLK
Merman Fighter,Merman Hunter,Mermaid Initiate#enddef

#define UNITS_NAGAS
Naga Fighter,Saurian Augur,Saurian Skirmisher#enddef

#define TERRAIN_KEEP
Khw#enddef

#define TERRAIN_WATER
Ww#enddef

#define TERRAIN_WATER_DEEP
Wo#enddef

#define START_P1
x,y=4,7
#enddef

#define START_P2
x,y=30,7
#enddef

#define KEEP_P1M
x,y=4,5
#enddef

# ...here are the other keeps...

#define CASTLE_P1M
x= 3, 3, 4, 4, 4, 5, 5
y= 5, 6, 4, 5, 6, 5, 6
#enddef

#define CASTLE_P1N
x= 3, 3, 4, 4, 4, 5, 5
y= 9,10, 8, 9,10, 9,10
#enddef

#define CASTLE_P2M
x=29,29,30,30,30,31,31
y= 5, 6, 4, 5, 6, 5, 6
#enddef

#define CASTLE_P2N
x=29,29,30,30,30,31,31
y= 9,10, 8, 9,10, 9,10
#enddef

[multiplayer]

 # ...here is what we wrote previously...

#define SELECT_FACTION SIDE KEEP UNITS OTHER_KEEP OTHER_CASTLE START
 # ...you know what...
#enddef

{SELECT_FACTION 1 {KEEP_P1M} {UNITS_MERFOLK} {KEEP_P1N} {CASTLE_P1N} {START_P1}}
{SELECT_FACTION 1 {KEEP_P1N} {UNITS_NAGAS}   {KEEP_P1M} {CASTLE_P1M} {START_P1}}
{SELECT_FACTION 2 {KEEP_P2M} {UNITS_MERFOLK} {KEEP_P2N} {CASTLE_P2N} {START_P2}}
{SELECT_FACTION 2 {KEEP_P2N} {UNITS_NAGAS}   {KEEP_P2M} {CASTLE_P2M} {START_P2}}

[/multiplayer]

(C) 2008 Viliam Búr viliambur.blogspot.com

If you got lost in the "..." notes, you can see the complete code at the end of this article. (I am trying to keep the article short, not very successfully.) The essense is that macro without parameters is called like this: "{MACRO_NAME}", and the macro with parameters is called like this: "{MACRO_NAME PARAM1 PARAM2 PARAM3}". You can use a macro as a parameter for another macro like this: "{MACRO_NAME {OTHER_MACRO}}.

Some macros have the "#enddef" written immediately after the text. This way the "end of line" symbol is not included in the macro. I use this for macros which are only a part of line in the script; then I could put more such macros in one line, like this: "recruit={UNITS_MERFOLK},{UNITS_NAGAS}". If a macro contains full lines of script, it does not matter if there are too many "end of line" symbols, because the empty lines are ignored.

Merfolk keep selected screenshot

At this moment, the scenario is ready. The only problem is that AI (artificial intelligence, also called "Computer Player" in Wesnoth) does not understand our system, so from its point of view our keeps are just ordinary keeps same to each other; and because of some technical details, it always selects the northern keep. Therefore we will add some AI logic.

We cannot explain the details of our design to AI and expect that it will understand the consequences and develop the correct strategy. (If anyone is capable of programming such smart AI, please contact the Wesnoth development team.) Instead we will use a trick: we will decide for some keep and destroy the other one, and then the AI will naturally "select" the remaining keep (which will trigger the event as usually).

The decision algorithm will be like this: choose the keep randomly. Oh, did you expect something smarter? So let's do this: if the computer player is player 1, it will choose randomly. If the computer player is player 2, it will choose the opposite of player 1. However if the player 1 did not choose any keep during the first turn (which is strange, but possible), the computer player 2 will choose randomly, too. Now let's rephrase this differently: The computer player will choose randomly, but the computer player 2 will take the opposite of player 1 instead, if player 1 has already chosen.

We will need the "ai turn" event, which is triggered at the beginning of AI side turn. However we will use random numbers, and for some technical reasons it is not allowed to generate random numbers during "ai turn" event, so we will also use the "side turn" event; this is triggered at the beginning of any side turn (before "ai turn"). In "side turn" event we will generate the random number and put it into variable, then "ai turn" event we will use it; or it will be ignored for human players.

The random number will be stored in a variable called "choice"; technically it will be a number 1 or number 2, but to make the script more legible, we can create macros CHOOSE_MERFOLK and CHOOSE_NAGAS with values 1 and 2, so we do not have to remember which number means which choice.

There will be new tags "[set_variable]" for storing a value in a variable; "[if]" and "[then]" for decisions; "[variable]" for checking if a variable contains the specified value; "[have_location]" for checking if a location contains (or does not contain, if we use the "!" symbol) the specified terrain. We will also use event variables "turn_number" and "side_number" to determine if this is the first turn, and if this is player 2.

In other programming languages the following algorithm would look somehow like this. (This is just an imaginary pseudocode based on some existing languages.)

on SideTurn (turn) {
 if turn == 1 {
  choice := random(1..2)
 }
}

on AITurn (turn, side) {
 if turn == 1 {

  if  side == 2  and  terrain(KEEP_P1M) != TERRAIN_KEEP  {
   choice := CHOOSE_MERFOLK
  }
  if  side == 2  and  terrain(KEEP_P1N) != TERRAIN_KEEP  {
   choice := CHOOSE_NAGAS
  }

  sink_keep(side, not choice)
 }
}

And this is the full scenario script:

#define UNITS_MERFOLK
Merman Fighter,Merman Hunter,Mermaid Initiate#enddef

#define UNITS_NAGAS
Naga Fighter,Saurian Augur,Saurian Skirmisher#enddef

#define TERRAIN_KEEP
Khw#enddef

#define TERRAIN_WATER
Ww#enddef

#define TERRAIN_WATER_DEEP
Wo#enddef

#define START_P1
x,y=4,7
#enddef

#define START_P2
x,y=30,7
#enddef

#define KEEP_P1M
x,y=4,5
#enddef

#define KEEP_P1N
x,y=4,9
#enddef

#define KEEP_P2M
x,y=30,5
#enddef

#define KEEP_P2N
x,y=30,9
#enddef

#define CASTLE_P1M
x= 3, 3, 4, 4, 4, 5, 5
y= 5, 6, 4, 5, 6, 5, 6
#enddef

#define CASTLE_P1N
x= 3, 3, 4, 4, 4, 5, 5
y= 9,10, 8, 9,10, 9,10
#enddef

#define CASTLE_P2M
x=29,29,30,30,30,31,31
y= 5, 6, 4, 5, 6, 5, 6
#enddef

#define CASTLE_P2N
x=29,29,30,30,30,31,31
y= 9,10, 8, 9,10, 9,10
#enddef

#define CHOOSE_MERFOLK
1#enddef

#define CHOOSE_NAGAS
2#enddef

[multiplayer]

 id=multiplayer_2p_Pool
 name= _ "2p - Pool"
 description= _ "Water battle between merfolk and nagas."
 map_data="{@campaigns/Examples_by_Viliam/maps/multiplayer/2p_Pool.map}"

 {DEFAULT_SCHEDULE}
 {DEFAULT_MUSIC_PLAYLIST}

 [side]
  side=1
  controller=human
  canrecruit=yes

  team_name=west
  user_team_name= _ "teamname^West"
 [/side]

 [side]
  side=2
  controller=human
  canrecruit=yes

  team_name=east
  user_team_name= _ "teamname^East"
 [/side]

 [event]
  name=prestart
  [label]
   {KEEP_P1M}
   text=_"Merfolk"
  [/label]
  [label]
   {KEEP_P1N}
   text=_"Nagas"
  [/label]
  [label]
   {KEEP_P2M}
   text=_"Merfolk"
  [/label]
  [label]
   {KEEP_P2N}
   text=_"Nagas"
  [/label]
 [/event]

#define SELECT_FACTION SIDE KEEP UNITS OTHER_KEEP OTHER_CASTLE START
 [event]
  name=moveto
  [filter]
   side={SIDE}
   {KEEP}
   [filter_location] 
    terrain={TERRAIN_KEEP}
   [/filter_location]
  [/filter]
  first_time_only=yes

  [set_recruit]
   side={SIDE}
   recruit={UNITS}
  [/set_recruit]
  [label]
   {OTHER_KEEP}
   text=""
  [/label]
  [terrain]
   {OTHER_CASTLE}
   terrain={TERRAIN_WATER_DEEP}
  [/terrain]
  [terrain]
   {START}
   terrain={TERRAIN_WATER}
  [/terrain]
 [/event]
#enddef

{SELECT_FACTION 1 {KEEP_P1M} {UNITS_MERFOLK} {KEEP_P1N} {CASTLE_P1N} {START_P1}}
{SELECT_FACTION 1 {KEEP_P1N} {UNITS_NAGAS}   {KEEP_P1M} {CASTLE_P1M} {START_P1}}
{SELECT_FACTION 2 {KEEP_P2M} {UNITS_MERFOLK} {KEEP_P2N} {CASTLE_P2N} {START_P2}}
{SELECT_FACTION 2 {KEEP_P2N} {UNITS_NAGAS}   {KEEP_P2M} {CASTLE_P2M} {START_P2}}

 [event]
  name=side turn
  first_time_only=no

  [if]
   [variable]
    name=turn_number
    numerical_equals=1
   [/variable]
   [then]
    [set_variable]
     name=choice
     rand=1..2
    [/set_variable]
   [/then]
  [/if]
 [/event]

 [event]
  name=ai turn
  first_time_only=no
  [if]
   [variable]
    name=turn_number
    numerical_equals=1
   [/variable]
   [then]

    [if]
     [variable]
      name=side_number
      numerical_equals=2
     [/variable]
     [have_location]
      {KEEP_P1M}
      terrain=!,{TERRAIN_KEEP}
     [/have_location]
     [then]
      [set_variable]
       name=choice
       value={CHOOSE_MERFOLK}
      [/set_variable]
     [/then]
    [/if]

    [if]
     [variable]
      name=side_number
      numerical_equals=2
     [/variable]
     [have_location]
      {KEEP_P1N}
      terrain=!,{TERRAIN_KEEP}
     [/have_location]
     [then]
      [set_variable]
       name=choice
       value={CHOOSE_NAGAS}
      [/set_variable]
     [/then]
    [/if]

#define PRESELECT_FACTION SIDE CHOICE OTHER_KEEP
    [if]
     [variable]
      name=side_number
      numerical_equals={SIDE}
     [/variable]
     [variable]
      name=choice
      numerical_equals={CHOICE}
     [/variable]
     [then]
      [terrain]
       {OTHER_KEEP}
       terrain={TERRAIN_WATER}
      [/terrain]
     [/then]
    [/if]
#enddef

{PRESELECT_FACTION 1 {CHOOSE_MERFOLK} {KEEP_P1N}}
{PRESELECT_FACTION 1 {CHOOSE_NAGAS}   {KEEP_P1M}}
{PRESELECT_FACTION 2 {CHOOSE_MERFOLK} {KEEP_P2N}}
{PRESELECT_FACTION 2 {CHOOSE_NAGAS}   {KEEP_P2M}}

   [/then]
  [/if]
 [/event]

[/multiplayer]

This scenario (with some improvements not mentioned in these articles) can be downloaded as a Wesnoth add-on "Examples by Viliam".

Unfortunately in Wesnoth 1.4 there is a bug which causes AI problems with recruiting if it has some nontypical recruitment list, such as here. So the computer player will not recruit units, or just not recruit enough. This should not discourage you, it is just a reminder that the game is always developing (I have reported the bug and hope it gets fixed someday in 1.6), and if you do something unusual, you may need to get in contact with the game developers. If your scenarios are more similar to the existing ones, the chance of finding a bug is smaller. At least this scenario should work correctly when played human against human.

Related articles:

Labels:

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home