Introducing: PyScenario - easy-to-use scenario event scripting for RFC

The only "problem" thus far. And its not really any kind of problem with RFC:E/M but rather with the game as such. I just wasn't aware of the rounding thing, which strikes me as pretty ********. Especially since you only can fire Python code at the end of a game turn. (Also ******** that the call is "onBeginGameTurn". :crazyeye:)
 
Another progress report:

I've abandoned both turn() and year() Conditions in favor of the brand new date() method:

date( date1, date2=None )
The arguments can be formatted in any of these configurations:
date(-1000,1000) - creates an turn interval from the first turn 1000 BC - or beyond - to the turn AD 1000.
date(-1000.0) - fires once when the date 1000 BC appears - or passes.
date("2000.0") - fires once when the game first game turn with the date AD 2000 - or beyond - appears.
date("2000.5") - fires once on the first game turn with the June of AD 2000 - or beyond - passes.
date("2000:6") - fires once on the first game turn with the July of AD 2000 - or beyond - passes (Same as above.)
date("2000:3","2000:9") - creates an turn interval from the turn first turn with the date March of AD 2000 - or beyond - to September AD 2000.

The waypoints() method now also takes the same format arguments.
 
Also, the label() method is gone and there is a new player() method. I've explained this already in the thread, but the new documentation will of course be even more specific. It will however be possible to use old PyScenario scripts with the new version by adding this line into the Scenario module:
Code:
Trigger = Compatibility
You could basically keep scripting the same way as ever, but then some things wouldn't be up to par with the RFC Epic/Marathon version of RFC. Like the waypoints() method wouldn't work as intended, but would rather still be used for defining game turn waypoints - instead of the new date values.

Now I'll take another look at what can be done with all the unit spawning Actions within the current PyScenario setup.
 
Ok, so we have tree different methods for spawning units then:

units( iX, iY, eType=None, iNum=1 )
garrison( tCoords=None, eType=None, iNum=2 )
spawn( eType, iNum=1, bRandom=True, bForeign=False )


If we break them down by arguments instead:
iX
iY
tCoords
eType
iNum
bRandom
bForeign


If we're only gonna have one general purpose units spawning method then we have to decide on how to define the target tile. The units() method has separate arguments for the X and Y coordinates - and this is to make it easier to use the method. The tCoords argument in the garrison() method does the same thing, but it requires the user to put the coordinates inside a data structure (tuple). On the other hand that setting is optional - I don't think we need to make the use of tuples obligatory.

Perhaps we should just resign with having to define the coordinates outside of the actual unit spawning method? Since there would be no arguments for setting the target tile you'd pretty much have to resort to using the target() method instead.

But there is another issue to consider, also. The units() method can be used several times in one single Trigger - with different coordinates (iX and iY values)! The application actually goes to some trouble making this happen - but do we even need this added functionality? Is it really that convenient to spawn units on several tiles at once? (Otherwise that would involve using several Triggers to make a multi-tile event happen.)

Furthermore, the garrison() method has a added feature where it can be used on entire map areas = plot lists. While the units() and spawn() methods only spawn units on one location when used with a plot list, the garrison() method will spawn units on all city tiles within that area. I believe this to be a very handy feature, but perhaps it could be activated with some boolean setting instead?

The new spawn() method also comes with a pair of boolean settings (bRandom and bForeign) for use with plot lists. These could probably be transferred to a unified unit spawning setup also.

The eType and iNum values could have None and 1 respectively as default values, so this is not a big issue. I think that the default values aren't getting much use anyway, as most scenario makers seem to have a pretty strong view on what type of units and how many of them they need spawn...

So I guess I have something to work with then. And some work to do. :p

Oh, and another idea: What about adding something like a select() method for finding valid map tiles for spawning from a plot list? Then there would be no need to have built-in Conditions in the spawning methods, as the plot list wouldn't be valid anymore and there would only be one target tile. I guess it could, somehow, work together with the valid() method... Hmm...
 
Ok, this is what the new units() method looks like - it replaces the current units(), garrison() and spawn():

units( tCoords=None, iNum=1, eType=None, bGarrison=False )
So its all optional arguments then. The tCoords argument of course replaces the iX and iY arguments of the old method, but the target() method is probably the default way to define a target tile anyway. This also means that there is no way to spawn on multiple tiles within the same Trigger, even if you define the tCoords argument differently. (Its always the latest tile defined that is tagged.) So this is actually taking away functionality from PyScenario - opinions?

The eType argument is optional as always, but the bGarrison boolean argument is used to determine what the default eType=None will produce (conscript unit type or not). But bGarrison will also enable the current functionality of the garrison() method for spawning units in multiple cities from a plot list!

Talking about plot lists, the way to spawn units on random tiles now requires the use of the valid() method in conjunction with target() and/or tiles(). The augmented valid() method looks like this:

valid( tCoords=None, bDomestic=False, bForeign=False, bLand=True, bWilderness=False, bAdjacent=False, bCity=False, bRandom=True )
The last two arguments are new and they should enable all the possible options that are currently available with the unit spawning methods. The difference is that when a plot list is present the valid() method will no longer look for a single target tile, but will rather loop though the plot list - and if it finds a valid tile (according to the settings available) - then this tile will become the target tile (at least as far as the units() method is concerned).

I'm yet to test any of this and there might be slight changes in the actual released version, but this is the general idea. Now would be a good time to voice any concerns or present any suggestions.
 
All the changes discussed here over the summer have been implemented and the PyScenario Beta v2 is available from the beta release thread. PyScenario is now only supported for the RFC Epic/Marathon mod-mod, so you should get that as well.

Note that the documentation on the first page of this thread is completely revised and up-to-date with all the changes. Unfortunately I haven't been able document what exactly has changed from the previous version, so you'll have read up on the documentation in order to do the conversion.

As it has been impossible to test the whole application with the limited time I had to do the conversion - and to rewrite the documentation - there will inevitable be bugs. But there will also be hot-fixes as soon as we find those bugs. Please report any issues in the beta testing thread!
 
Quick question,
Does (for instance) January 2000 = date(2000.0) or date(2000.1)?
Likewise, does December 2000 = date(2000.11) or date(2000.12)?

Oh no sorry, I read your documentation wrong. Decimal scale would make January 2000.0 and December ~2000.916?
So I should instead use strings, like so: "2000:1", "2000:12"?
 
Yeah, I think you deciphered it alright.

Good to have you back on-board, by the way! :goodjob:
 
Sorry to be a pain in the butt, but to convert my old scenario.py to the new style, what do I need to change exactly?

I've changed all turn() conditions to their corresponding date() conditions. Are other triggers changed?

Also, how do interval settings now work? For example, I had this:
Code:
Trigger(iSpain).check().turn(281,301,10,-1).city(25,32,iSize=3).name("Panamá").culture(50).buildings(lColonialBuildings).religions(iChristianity).garrison(eType=iMusketman)
Which meant that there would be a ~10% chance each turn between 1500AD and 1600AD that Panama would spawn for Spain (the historical date is 1545AD if I recall correctly, so on average that's when the city would spawn, around turn 291.

Would changing it to this do the same thing?
Code:
Trigger(iSpain).check().date(1500,1600).interval(10,True,True).city(25,32,iSize=3).name("Panamá").culture(50).buildings(lColonialBuildings).religions(iChristianity).garrison(eType=iMusketman)
Since bRandom=True makes the odds of the trigger to be 1/iInterval, I figured I just set the interval to what it used to be for normal game speed, and then the script would adapt this for me in Epic/Marathon. Is this right?
 
Regarding the decimal dates: It should only be useful to use .0, .25, .50 and .75 as game dates aren't AFAIK fractioned beyond quarter years. But I'm not sure if you should use .0, .3, .6 and .8 instead to avoid rounding down issues. I guess you could test it yourself to find out.

Other than that, using the data string with months is always valid. But it still would really only make sense to use ":1", ":4", ":7" and ":10". But if you wanna script a German war declaration on Poland (independents?) on September of 1939 you can do that: "1939:9" - it should fire on the appropriate turn regardless of game speed.
 
Sorry to be a pain in the butt, but to convert my old scenario.py to the new style, what do I need to change exactly?
I'd have to look it up myself to make sure I didn't give you the wrong information, but the main changes were discussed (well I mentioned them) in this thread leading up to the RFCM port.

I you read the last few pages you'd pretty much be up to speed.

The important things would probably be that the Target Player isn't defined within the Trigger constructor anymore. :eek: You use the player() method instead. Trigger labels are now defined within the Trigger itself, but those are optional.

Also, there is only one Action for spawning units - the units() action. All garrison() methods need to be turned into units() methods instead.

I believe that this is what your converted Trigger should look like:
Code:
Trigger([COLOR="Red"]).player([/COLOR]iSpain).check().[COLOR="Red"]date(1500,1600).interval(10,True,True)[/COLOR].city(25,32,iSize=3).name("Panamá").culture(50).buildings(lColonialBuildings).religions(iChristianity).[COLOR="Red"]units[/COLOR](eType=iMusketman)
So I guess you got the interval() Condition right then. :D

There are some new features also so you'd wanna take the time to look through the documentation once you got the time.
 
Code:
valid( tCoords=None, bDomestic=False, bForeign=False, bLand=True, bWilderness=False, bAdjacent=False, bCity=False, bRandom=True )
I think I am reading this trigger wrong. I want the condition to NOT fire if the tile is within anyone's borders, however if the city is adjacent to some borders (the target civ or anyone else's) I don't care. Is this fine?

Code:
Trigger().player(iEngland).check().valid(None,False,False,True,False,True,False,True).date(1600,1730).interval(30,True,True).city(93,29,iSize=3).name("Colombo").culture(50).buildings(lColonialBuildings).religion(iChristianity).units(eType=iMusketman)
Another thing I've been trying to do: since the AI doesn't like to colonise some fairly prominent spots such as Colombo, I thought I could make a random spawn trigger. In the case of Colombo, colonisers could be England/Netherlands/Portugal. Since I don't want it to be too deterministic, I figure it would be nice if the odds of spawn were something like:
England: 33%
Netherlands: 33%
Portugal: 33%
Maybe later when I've worked out a bit more I can try and make it so that there are also odds that nothing spawns. Would this work, do you think?
Code:
#Colombo
Trigger().player(iEngland).check().valid(None,False,False,True,False,True,False,True).date(1600,1730).interval(30,True,True).city(93,29,iSize=3).name("Colombo").culture(50).buildings(lColonialBuildings).religion(iChristianity).units(eType=iMusketman)
Trigger().player(iNetherlands).check().valid(None,False,False,True,False,True,False,True).date(1600,1730).interval(30,True,True).city(93,29,iSize=3).name("Colombo").culture(50).buildings(lColonialBuildings).religion(iChristianity).units(eType=iMusketman)
Trigger().player(iPortugal).check().valid(None,False,False,True,False,True,False,True).date(1660,1730).interval(30,True,True).city(93,29,iSize=3).name("Colombo").culture(50).buildings(lColonialBuildings).religion(iChristianity).units(eType=iMusketman)
Now between 1600 and 1730 there are 30 turns (in normal mode, I assume pyscenario can scale for us on its own). Therefore every turn starting in 1600AD there is a 1/30 chance that Colombo will spawn for England, 1/30 for Dutch, et cetera. All this adds up to a 10% (3/30) chance that Colombo will spawn for anybody on a given turn between 1600AD and 1730AD, which means the average spawn time will be 1650AD, with 1/3 chance it could belong to either of the three nations. Correct?

Now, we've established there are 30 turns in the interval. I take it that to "create" odds of nothing spawning, I just have to increase the interval without increasing the amount of time. If I make iInterval = 90 for instance. Then the odds that a city could spawn on a given turn in 1600-1730AD are 3/90, or 1/30. This means the average spawn date is 1730AD (the end of the interval) and therefore there is a slight chance nobody will found Colombo. Am I on the right track?

This is also the reason I'm trying to make the "valid()" trigger work correctly. I don't want cities on the same/adjacent tile to be erased... but I don't want plot ownership to affect colonisation either. Does bDomestic/bForeign need to equal False or True so that the condition passes?
 
Regarding the valid() Condition: bDomestic=True requires the tile to belong to the Target Player. (False does nothing.) bForeign=True allows the tile to belong to another Civ than the Target Player. (False requires the tile not to belong to any player - except the Target Player. No owner is also valid - as the tiles isn't foreign.)

If you don't want to spawn cities that replace other you leave the bCity=False default setting. (A True setting will allow city tiles - no matter who owns the city.)

So the default settings prohibit spawning (or whatever) on foreign tiles - and they prohibit spawning on - or next to - city tiles.

Other than that, you only have to specify the setting you change - there is no need to type all 8 settings into each method. (Unless you want to.)

So if you're gonna use the default values for everything but bForeign, for example, you only have to do:
Code:
valid(bForeign=True)
Or if you wanna enable city tiles you go:
Code:
valid(bCity=True)
I hope this cleared it up a bit.

Regarding the probability thing I guess your logic is correct. My math isn't what it should be, so someone else might be able to check it for you.

edit: Also, you don't need to use the name() method when spawning cities. You can just go:
Code:
city(93,29,"Colombo",3)
 
The random colony Trigger-cluster-thing gave me an idea. What about a random player method? You supply it any number of players (or a list containing players) and it picks one at random. But what would such a method be called? What about:

random( <enumerated values> )

Other than that, if you wanna have several - more or less - identical Triggers you could use a little Python to aid you:
Code:
[B]colombo = [COLOR="Red"]"[/COLOR][/B]check().valid(None,False,False,True,False,True,False,True).date(1600,1730).interval(30,True,True).city(93,29,iSize=3).name([B][COLOR="Red"]'[/COLOR][/B]Colombo[B][COLOR="Red"]'[/COLOR][/B]).culture(50).buildings(lColonialBuildings).religion(iChristianity).units(eType=iMusketman)[B][COLOR="Red"]"[/COLOR][/B]

Trigger().player(iEngland).[B]eval(colombo)[/B]
Trigger().player(iNetherlands).[B]eval(colombo)[/B]
Trigger().player(iPortugal).[B]eval(colombo)[/B]
Note that I had to change the double quotations to single quotations inside the string!
 
So we'd have something like Trigger().player(random(iEngland, iNetherlands, iPortugal)). ?

Does eval(<string>) basically mean "copy and paste whatever this string is defined as earlier" ?
 
So we'd have something like Trigger().player(random(iEngland, iNetherlands, iPortugal)). ?
Nope, just:
Trigger().random(iEngland, iNetherlands, iPortugal)...
I guess the method could be called randomPlayer() but that would brake the naming convention of PyScenario. I'll see if I can't whip something up tomorrow night. (It'll be a custom module then - I don't know if you ever used one of those?)

Does eval(<string>) basically mean "copy and paste whatever this string is defined as earlier" ?
I think "eval" is short for "evaluate" - its a function that tries to run the contents of a string as it were code, or something.

But you'd have to try it - I tend to use eval() wrong myself, so it may not work. (I have done something similar with good results earlier. Sadly I don't have the code to check what exactly I did.) It probably works, ok? :p
 
Actually, your random() example isn't half-bad... Putting a random() function inside a PyScenario method would work for any method, not just for picking a player... Like buildings(random(1,2,4)) or religions(random(0,3)).

But it would move PyScenario closer to programming, which is not the aim here. (Quite the opposite, actually.)

I could however include that also - as an undocumented easter-egg of sorts. :D
 
Another idea for the random colony setup: If you use the owned() method for the city tile but no Target Player it should set the Target Player according to who owns the tile.

So this could be your first option - and if no city spawned you could have a random interval spawn on the same location. Preferable with a random Target Player.

Or something.
 
Ok, a Custom module is attached. Unpack the file into the \RFCMarathon\Assets\Python\ folder and add these lines to your Scenario module:
Code:
from Custom import *
Trigger.random = random

random( <enumerated values> )

This method sets a randomly selected Target Player from the supplied settings. You can use any number of integer values (or a list containing said values) corresponding to valid players. (You can increase the chance of a player by adding it several times - otherwise the odds for each entry will be equal.)

Note however that the random selection is only done once - when the script initializes. So it would be a different player each time you play the scenario - not every turn or every time the Trigger fires. (Do we need to change this so that the selection is done every time the Trigger is checked instead?)

This is a prototype so test it and get back to me with the results of your efforts. If this is something we want in PyScenario it will be added to the next update.

edit: I added a randomize() function also - it works for all methods - not just player(). As its not a class method it can be used inside a method. So this is what was described above:
Code:
Trigger().random(1,2,4)...
This does the same:
Code:
Trigger().player(randomize(1,2,4))...
Or you could assign the random player to a variable:
Code:
eRandomPlayer = randomize(1,2,4)
And then use the variable instead:
Code:
Trigger().player(eRandomPlayer)...
And finally, this is also valid:
Code:
lPotentialPlayers = [1,2,4]
eRandomPlayer = randomize(lPotentialPlayers)
Trigger().player(eRandomPlayer)...
Or just:
Code:
Trigger().player(randomize([1,2,4])...
 

Attachments

Back
Top Bottom