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

Bad news - the latest version of RFC:E/M doesn't include anything like what I did with RFC Fast...

So that would have to be included before I would consider RFC:E/M "completed". But I guess I could "help out" with that, then...

Could someone please give RFC Fast Edition proper testing!
 
There are technical issues to look into however, before I can even decide. Like how the turn() method would work, because RFC:E/M throws any set game turn setup right out the window. It seems that the year() method would become the default game turn Condition instead. Which isn't optimal since game year intervals vary during a game.

If I may help at all, this is solved with getTurnForYear function which always returns the same turn for a given year, i.e. if you base a trigger on getTurnForYear(1602) it's not checking for year 1602, but a single turn closest to that year, so it's essentially the same as if you checked for a fixed turn. Such trigger will fire only once on the turn closest to 1602, even if 1602 is not on the timeline (because turns go 1600, 1605), it will also fire only once if you have multiple seasons/months of 1600.

It is also somewhat problematic that one of the core features of PyScenario is the ability to have Triggers fire on intervals/randomly. If you change the game speed, then the scenario gets thrown out of whack. And if the interval/probability scales with game speed - then you'd pretty much have to design your scenario with Normal speed turn intervals. This would make actual scenario making problematic, as I believe that most scenario makers would opt for a game speed other than Normal.

The only problem is, like you said, with smallest intervals and Epic speed (1.5x multiplier), but RFC events, or even unmodded BTS already have the same problem i.e. in BTS Anarchy should last 1 turn on Normal, 1.5 turns on Epic and 3 turns on Marathon - but of course it can't last 1.5. utils.getTurns() does the conversion, though it has some false-rounding built-in to maintain proper intervals in Stability.py

For the pickling thing I'm PMing you.
 
There are actually two separate game turn Conditions: turn() and year(). It would pretty much only make any sense to use the year() method within a RFC:E/M context, but turn intervals are exactly what they sound like and there would still need to be a turn() - or some other - method for this.

My own solution would pretty much be to force all scenarios to made for Normal speed (as now) - and then the mod would adapt all game turns and intervals dynamically. It should work but it wasn't something I was considering when I designed the application. It would probably not be optimal for the scenario maker, since he'd have to calculate what the game turn intervals would be on Marathon speed.

Another solution would be to not fix it at all. If a scenario is made for Marathon speed - then you'd have to live with defunct intervals on any other speed. The scenario would probably still be playable. And the entire script could probably be restricted to one speed only, so it wouldn't even be an issue. But then I'd have to add a function call to the Scenario module template, and it would be required by the scenario maker to figure out how to change the setting.
 
My own solution would pretty much be to force all scenarios to made for Normal speed (as now) - and then the mod would adapt all game turns and intervals dynamically. It should work but it wasn't something I was considering when I designed the application. It would probably not be optimal for the scenario maker, since he'd have to calculate what the game turn intervals would be on Marathon speed.

But that is how RFCMarathon code works, and I still can't see what is the problem - why calculate anything. The code - originally Rhye's - is for Normal speed, and the two helper functions convert it on the fly to the current speed. That's all.

In fact, you could use PyScenario with RFC:E/M without any changes, right now, just by using the two helper functions as arguments for turn(), and it will work fine.

For example:
Code:
Trigger(2).check(None).turn(181,190,5,-1)

will work properly with all speeds if you use:
Code:
Trigger(2).check(None).turn(getTurnForYear(600), getTurnForYear(700), utils.getTurns(5), -1)

so in reality it's all about making it more user-friendly, such as by making conversion inside your turn() method, renamed to year() (old year() would be redunant):
Code:
Trigger(2).check(None).year(600,700,5,-1)

So that the user just types years, turn intervals as they would be in normal game, and the helper functions do the rest. Only problem is when you want something to happen exactly 1 turn after a given date, in such cases I use getTurnForYear(600)+1, though that's something that can be easily fixed with some different method/arguments.


[*] 700 or whatever turn 190 would be, was guessing
 
Also, you could make PyScenario work with both "vanilla" RFC and E/M with a single switch that would enable/disable the use of helper functions.
 
Yeah, I understand all this. But the way I view scenario making is that you wanna be able to set some thing to happen say every ten game turns. But then that would only be valid for the default game speed - and the results would be somewhat unpredictable for the other speed's. I guess it would be really bad for short intervals... (Like you design you scenario to perform something every other game turn at Marathon speed - then this event will take place on every turn on the other speeds!)

Of course PyScenario can - and will eventually be - adopted to RFC:E/M. It will probably only be available for this next generation of RFC in the end. And there is no reason to even use the RFC:E/M conversion code - I probably wanna add my own anyway. (To get more control over rounding and such.)

Its just at matter of a number of design choices I have to make, and I thank you for your input. What would the actual beta-testers prefer, by the way? Should the turn() Condition be taken out of the API (it can stay in the application as an undocumented feature though) and the year() Condition be augmented with interval parameters? (But would those be year or turn intervals, in that case?)

I'm actually kinda leaning on letting the scenario designer decide what game speed is default with his or her scenario. If players wanna mess around with some other game speed - that would be their mistake. In that case everything can pretty much stay the same as now - with no scaling of turn intervals. (The good thing being that this retains total control for the scenario maker - to the hell with people who can't read instructions... :rolleyes:)
 
Yeah, I understand all this. But the way I view scenario making is that you wanna be able to set some thing to happen say every ten game turns. But then that would only be valid for the default game speed - and the results would be somewhat unpredictable for the other speed's. I guess it would be really bad for short intervals... (Like you design you scenario to perform something every other game turn at Marathon speed - then this event will take place on every turn on the other speeds!)

The results are not unpredictable, they're converted just like everything in CIV4. CIV4 is designed to produce X amount of Barbs, every Y turns etc. But that X and Y are multiplied by the modifiers GameSpeedInfos XML, like hundreds of other values in game, from food to anarchy length. Things may be a little off sometimes, but that is the case with everything not just in RFC:E/M, but in whole CIV4, as in the Anarchy turns example.

And again, you don't design something at Marathon speed. You design everything at Normal speed, and let helper functions convert it. This way you never run into anything like you've mentioned. Like I said, whole CIV4, as well as RFC, are designed for Normal speed. All other speeds are just modifiers. You - the desginer - never even think of any "speeds". This isn't evident in RFC as Rhye just skipped it, but the original DLL code has all the relevant values always multiplied by values from GameSpeedInfos.XML

Of course PyScenario can - and will eventually be - adopted to RFC:E/M. It will probably only be available for this next generation of RFC in the end. And there is no reason to even use the RFC:E/M conversion code - I probably wanna add my own anyway. (To get more control over rounding and such.)

Mind you, the main function is in the DLL, though I can't see anything that needs changing there, it just goes by calendar, no rounding as such.
 
Yeah, I understand that the scenario would have to be designed for Normal speed, but most RFC scenarios would probably be tested and designed with Marathon speed in mind. So it would require some tweaking to get intervals exactly as the designer wants them, as the amount of game turns specified will be multiplied. And there will be rounding.

But I realize I'm making a hen out of a feather here - this really isn't my problem. What people do with PyScenario is their own headache. I can only give them the tools and document how it works.

Oh, and I meant the conversion of game turn length, not what year corresponds to what game turn.

But its probably a good thing that I have to think about these issues now, as I was planning on porting PyScenario to the BUG mod. There will also be different game speeds to take into account.
 
Ok, embryodead has updated RFC:E/M to v1.1 - with the requested anti-pickling included (or so I'm told - I'm downloading as of writing this). This pretty much means that I need to decide what version of RFC is to be used for beta-testing PyScenario.

So the next update will either be v1.031 (for RFC final patch) or v1.1 (for RFC:E/M). Does anyone not wanna move over to the unofficial version of RFC?
 
I can't wait for PyScenario: Marathon :) I finally fixed an error with invisible units (I uninstalled BTS, Warlors, vanilla, installed vanilla, Warlords. BtS, found a patch on a pendrive, installed the patch, found RFC, deleted RFC, saving PyScenario, unpacked RFC, in meantime scanned PC with antivir twice, uninstalled antivir, downloaded another one, scanned, checked forum and discover that I had somehow "bare map view" turned on) so I'm eager to mod :)
 
Good to have you back. :goodjob:

So I count one yay votes then. :king: No nay votes as of yet.
 
I'll have to take a closer look at RFC:E/M before I can make the decision, but hopefully I can get started this weekend.

Chances are also that I might take this opportunity to make some sweeping changes to PyScenario.

This will take some time in any case, since I have to test everything.
 
I'm thinking that the turn() method would have to go in an RFC:E/M version of PyScenario. It would still be there, though, and work all the same, but it would make no sense to use it since wouldn't adapt to the selected game speed. It wouldn't be documented and only allowed to exist for compatibility reasons.

With that said, I would like to propose a interval() Condition that replaces some of the features previously present in the turn() method:
interval ( iInterval, bScaling=True )

So you can set the turn interval to anything you like with iInterval - on Normal speed. It will however scale to the current game speed dynamically, unless you disable scaling with bScaling=False.

Also, there would be a random() Condition that fires at random intervals:
random ( iProbability, bScaling=True )

The change of the Condition passing is one in iProbablity but this scales with game speed. Unless, of course, the bScaling setting is changed.

Makes sense? Then the default Condition for firing Triggers on specific game turns would really be the year() Condition, which would work just as it always has. Its just that the rate at which the game date changes will vary from one speed to another.
 
Another possible change:

While I've grown accustomed to the way the target player is defined within a Trigger - in the Trigger-constructor itself - this could be changed. I originally designed it this way because I wanted it to be absolutely clear that one Trigger can only have one target player. Otherwise scenario makers will surely try to change the target player several times in one Trigger, which simply isn't possible with the basic design I've chosen for PyScenario.

But with that said, the ePlayer and eCivilization values could be moved into a player() method instead:
player ( ePlayer, eCivilization )

Furthermore, the Trigger constructor itself - typed Trigger() - could hold the label value from the current label() method. Like:
Trigger( label=None )

So having no label would be the default, but the tutorial would rather encourage users to give their Triggers unique (but optional) names. The label values are - of course - used for binding several Triggers into larger events, as covered in the documentation.

For compatibility purposes it would still be possible to use the old setup by adding a line like this before the Trigger lines:
Code:
Trigger = Compatibility
 
Looking at the RFC:E/M code I believe that a conversion would be very seamless as far as the actual coding goes. So I'd be able to complete the coding part of the work in one sitting and then focus on making any changes to PyScenario itself - and on testing.

I don't even believe that I need to use what is probably the main feature of the RFC:E/M code - the getTurnForYear() function. Because the only method dealing with dates would be the year() method, and that works in a quite different way. If I'm correct the year() method would stay unchanged, which helps.

I also wanna revisit what I said earlier this summer about trying to rework all the unit spawning methods into some other setup. I'm not sure what I wanna do at this point but what we have now is basically tree overlapping methods which all are designed for a specific purpose. One would be enough, not? But at the same time - we wouldn't wanna loose any features in the conversion...

Another thing, that isn't a priority of any kind, is to make it possible to spawn cities without specifying the city name. Because I could just have the code lookup a valid entry in the city name maps - if one doesn't exist for the Civ in question (as there are none for the minor Civs) then the code will simply use a city name from another Civ. The name would at least make sense geographically.

It could also be possible to spawn cities on a random tile, kinda how units can be spawned with a plot list instead of a target tile, and end up in a valid tile. So the city name would still make sense, as long as there is something in the city name maps.

This would make it possible to say spawn European cities at random, to fill gaps on the maps. The mind boggles. :D
 
I don't even believe that I need to use what is probably the main feature of the RFC:E/M code - the getTurnForYear() function. Because the only method dealing with dates would be the year() method, and that works in a quite different way. If I'm correct the year() method would stay unchanged, which helps.

No, you have to use that function, or your triggers won't fire, or they'll fire multiple times. You cannot rely on game year, especially not when dealing with multiple speeds, because, for instance, a date such as 230 AD may exist on one calendar, where years go 225, 230, 235... but not on the other, because years go 224, 228, 232... On the other hand, on Marathon, turns get down to 6 and 3 months, so your 2000 AD trigger will fire 2 or 4 times instead of 1.

That's what the function is for, it returns a single turn, closest to a given year, so a (single) trigger will always fire once, regardless of game speed used.
 
No, what I mean is that since I already solved this issue with my original year() code, it doesn't have to be changed:
Spoiler :
Code:
        def year(self, iYear1, iYear2=None):
                if not isValid(iYear2):
                        self.onceOnly()
                self.Conditions.appendEntry(CheckYear(iYear1, iYear2), self)
                return self

...

class CheckYear(Conditions):

        def __init__(self, iYear1, iYear2):
                self.setYear(iYear1, iYear2)
                self.notTurnTrigger = False

        def setYear(self, iYear1, iYear2):
                self.year1 = iYear1
                if isValid(iYear2):
                        self.year2 = iYear2
                else:
                        self.year2 = iYear1

        def check(self, pContext):
                pContext.debug(("CheckYear.check()", Game.getGameTurnYear(), self.year1, self.year2), None)
                return [B]Game.getTurnYear(pContext.args[0]+1) > self.year1 and Game.getGameTurnYear() <= self.year2[/B]
I'm not checking anything against a set date, but rather an interval of years. So it should be valid for any setup, really. Or so I believe.
 
Back
Top Bottom