Play, but check: how does the engine cheat designer



When designing a multiplayer game, almost the most important component is balance. The work of the game designer in this regard is similar to the work of an intelligence analyst: if he works well, no one notices. It is necessary to stumble, and the players shamelessly take advantage of the error. But the most interesting thing happens when, in addition to the game designer, the programmer is also mistaken ...


In this article we will consider one element of the Cossacks 3 strategy. In the game there are various types of musketeers and other shooters of the 17th and 18th centuries, as well as the opportunity to explore technologies that reduce the reload time of muskets. In total there are two improvements, each of which brings + 30% to the rate of fire - according to the game interface.


But even by eye it is clear that some combat units after the study of improvements shoot not just 60%, but even several times more often. When measuring the rate of fire directly using the built-in game timer, strange numbers come out that have nothing to do with the stated percentages.


Under the hood at the "Cossacks"


Fortunately, the game is made in a very friendly form for modders, so all the scripts we need are available as text files in the data / scripts / folder. Judging by the syntax, the scripts are written in Delphi or in a very similar language. Let's take a look at the mechanics of calculating the intervals between shots.


Notes
  • The analysis was conducted on the game "Cossacks 3" version 2.1.4.
  • All script segments listed below contain simplified pseudocode.

  1. At the start of the game, initialization of all combat units occurs. The procedure indicates the values ​​of vitality, cost and weapons for each type. For small arms, a parameter is passed indicating the interval between shots in game frames:


    //lib/unit.script procedure _unit_InitBase() 'musketeer' : maxhp := 70; SetObjBaseWeapon( x,x,x,x, 150, ... ); SetObjBasePrice( ... ); //lib/unit.script procedure SetObjBaseWeapon( x,x,x,x, pause, ... ) weapon.pause := _misc_FramesToTime( pause ); 

    Judging by the comments, the unit of time "game frame" is an atavism from the first "Cossacks", whose gameplay was copied during the creation of the third part. However, the frames are immediately converted into game seconds with a ratio of 1:32, and we no longer encounter them:


     //lib/misc.script function _misc_FramesToTime( val ) Result := ( val * gc_frames_to_time ); //dmscript.global gc_frames_to_time := 0.03125; gc_time_to_frames := 32; 

  2. Also, when starting the game, the data of the game nations is initialized, including the available improvements. For each of them, the value variable is indicated and stored, which, in the study of this improvement, affects the recalculation of the necessary game parameters:


     //lib/country.script procedure _country_Init() _country_AddUpgrade( x,x,x,x, type_attpauseperc, -30, ... ); procedure _country_AddUpgrade( x,x,x,x, upgrade_type, value, ... ); 

    In our case, this means that the intervals of combat units after each improvement are multiplied by 0.7 and then ... rounded up ?!


     //lib/player.script procedure _player_ApplyUpgrade() type_attpauseperc : weapon.pause := Round( weapon.pause * (1 + value/100) ); 

    Considering that initially the shooters' intervals are floating-point numbers in the range from 3.125 to 5.0, the decision to round the result of recalculation looks rather strange, if not to say important.


  3. After each shot, the delay is indicated before the next shot. The idividual.attackrate modifier applies to tower structures and in our case is always equal to 1.


     //lib/unit.script procedure _unit_ApplyAttackPause() attackdelay := weapon.pause * idividual.attackrate; 


So, in addition to the mathematical error in the calculations, the details of which can be read under the spoiler below, there is an inappropriate rounding of floating-point numbers. I wonder what effect this game has on the first look at a minor misstep on the mechanics?


A bit of math

The rate of fire is inversely proportional to the interval between shots. And if the number of shots per minute is important for the player, the game engine, as a rule, uses intervals to calculate the pause. The catch here is that “reducing the interval by 30%” and “raising the rate of fire by 30%” are completely different things. The ratio r between the intervals t and the number of shots n is described by a simple formula:


If, for example, we take an interval of 6 seconds (10 shots per minute) and decrease it by 30%, then we will not get 13 shots per minute:


To get the desired value, you should divide the current interval by the desired ratio of the new rate of fire to the old:



Method of measurement

To get the values ​​that the game engine works with, you can use the logging functions. To do this, you first need to enable the log entry:


  //cossacks.ini & editor.ini LogFileEnabled = true LogFileRoot = true 

And then at the end of the _unit_ApplyAttackPause () procedure, add a call to the Log () function:


  //data/scripts/lib/unit.script procedure _unit_ApplyAttackPause(const goHnd, weapind : Integer); begin //... if (attpause<>0) then Log(TObjProp(pobjprop).sid+' '+FloatToStr(attpause)); end; 

Now you can play with various arrows and improvements in the map editor (to activate the attack mode, press Ctrl + W ). The log will be written to a text file in the / log folder. After each shot made, the combat unit identifier and the value of its current interval will be recorded.


Who is who


Initially, the game scripts distinguish 35 types of shooters (not counting mercenaries who are not affected by the improvements). If we group them all by the size of the interval, then we will be able to distinguish ten categories. I decided to sort them by the relative increase in the rate of fire in order to single out those shooters who benefit most from the improvements. So, the results of the analysis:


Attack intervalShots / minIncrease rate of fire
Category Improvements0+1+20+1+2+1+2
I5.004.03.012.01520+ 25%+ 67%
II6.885.04.08.71215+ 38%+ 72%
III5.314.03.011.31520+ 33%+ 77%
IV5.634.03.010.71520+ 41%+ 88%
V3.753.02.016.020thirty+ 25%+ 88%
VI5.944.03.010.11520+ 48%+ 98%
VII4.063.02.014.820thirty+ 35%+ 103%
Viii4.383.02.013.720thirty+ 46%+ 119%
Ix4.693.02.012.820thirty+ 56%+ 134%
X3.132.01.019.2thirty60+ 56%+ 213%

In the diagram below, the columns correspond to categories I — X, from left to right. The last hatched column of the diagram corresponds to the rate of fire rate increase declared in the game interface. The left group of columns shows the increase in the rate of fire after one improvement, the right - after both.


List of categories and combat units

In the game there are various nations - 17 European and four unique (Ukraine, Turkey, Algeria and Scotland). The European factions are initially very similar and have musketeers and dragoons of the 17th and 18th centuries, as well as grenadiers. But sometimes the arrows of some nations differ from the template ones, or they are completely replaced by a unique type.


CategoryCombat units
IMusketeer 17c. (Austria)
Székely (Hungary)
Scottish Rifleman (England)
Commonwealth destruction (Poland)
Dragoon 18v. (Netherlands and Piedmont)
IIHuntsman (Switzerland)
Royal Musketeer (France)
IIIGrenadier (Europe except Denmark and Prussia)
Dragoon 18v. (Europe except France, the Netherlands and Piedmont)
Light trooper (different countries)
IVDragoon 17c. (Europe)
VMusketeer 17c. (Netherlands)
VIMusketeer 17c. (Spain)
Musketeer 18v. (Bavaria and Denmark)
Grenadier (Denmark)
Volunteer (Portugal)
Huntsman (France)
VIISerdyuk (Ukraine)
ViiiMusketeer 18v. (Saxony)
Grenadier (Prussia)
IxMusketeer 17c. (Europe except Austria, Poland, the Netherlands and Spain)
Covenant Musketeer (Scotland)
Sagittarius (Russia)
Yanychar (Turkey)
Musketeer 18v. (Europe except Denmark, Bavaria and Saxony)
Pandur (Austria)
Dragoon 18v. (France)
XMusketeer 17c. (Poland)
Haiduk (Hungary)

Notes:


  • The names of combat units copied from the Russian game interface.
  • Italicized arrows of the 18th century.
  • Bold marked horse arrows.

It turns out that the 17th century Polish musketeer and the Hungarian haiduk benefit most from improvements in the rate of fire: instead of the promised + 60%, they shoot more than three times more often. Due to the low initial value of the interval, they eventually shoot faster than any other shooter by two, three, or even four times.


Among the cavalry, the French dragoons of the 18th century were best settled: they receive a more than two-fold increase in the rate of fire. As a result, they make 50% more shots per minute than their counterparts from other European nations.


Naturally, the damage of a shot or damage per second is not taken into account, but even without this data it is obvious that combat units do not behave as planned.


How to fix

The quickest and most non-invasive solution to the problem is to rewrite the formula for applying the improvement. In addition to the rejection of rounding, instead of multiplying the interval by 0.3, divide it by 1.3. To do this, it’s enough to replace the formula with the following in the gc_upg_type_attpauseperc improvement processing procedure


  //lib/player.script Round(weapon.pause*(1+value/100)); 

on


  weapon.pause/(1+(-value)/100); 

Since the improvements are applied consistently, in the end, instead of the stated + 60%, we get + 69%. But it is still better than + 213%.


Afterword


In order to reliably identify miscalculations in the balance in this case, two more aspects of game mechanics should be analyzed - shooters' damage and economic value, along with the time required to create a combat unit. However, common sense suggests that you must first wait for the next update ...


I drew the idea for the study from the video “ Why Attack Rates in AoE2 Are Often Wrong ” (eng.), Which considers similar problems in the strategy of Age of Empires II .


UPD: Partially fixed error


Not even a week passed since the publication of the article, as the developers in update 2.2.1 fixed the error with rounding off. At the same time, the formula itself remained the same - the rate of fire grows by 43% for an upgrade. Since the calculation is incremental, after examining both improvements, all arrows work 104% faster.


Table

The rate of fire of units in shots per minute after examining both improvements, in ascending order:


Combat unitsShots
Huntsman (Switzerland)
Royal Musketeer (France)
17,8
Musketeer 17c. (Spain)
Musketeer 18v. (Bavaria and Denmark)
Grenadier (Denmark)
Volunteer (Portugal)
Huntsman (France)
20.6
Dragoon 17c. (Europe)21.8
Grenadier (Europe except Denmark and Prussia)
Dragoon 18v. (Europe except France, the Netherlands and Piedmont)
Light trooper (different countries)
23.0
Musketeer 17c. (Austria)
Székely (Hungary)
Scottish Rifleman (England)
Commonwealth destruction (Poland)
Dragoon 18v. (Netherlands and Piedmont)
24.5
Musketeer 17c. (Europe except Austria, Poland, the Netherlands and Spain)
Covenant Musketeer (Scotland)
Sagittarius (Russia)
Yanychar (Turkey)
Musketeer 18v. (Europe except Denmark, Bavaria and Saxony)
Pandur (Austria)
Dragoon 18v. (France)
26.1
Musketeer 18v. (Saxony)
Grenadier (Prussia)
28.0
Serdyuk (Ukraine)30.1
Musketeer 17c. (Netherlands)32.7
Musketeer 17c. (Poland)
Haiduk (Hungary)
39.2

Source: https://habr.com/ru/post/414853/


All Articles