Skip to main content
Topic: Messing around with speedbonuses (Read 9606 times) previous topic - next topic

Messing around with speedbonuses

I'm trying to consider speedbonuses for pak128.Britain, so I'd really appreciate someone explaining exactly how they work and what the calculation for revenue is that determines what you earn, and how speedbonus and goods price affect that.

One thing I'm considering is making the speedbonus the same for all modes, and adjusting vehicle maintenance accordingly (i.e. more revenue for fast planes compared to a bus, but faster planes have much higher maintenance so profit margins are similar, that sort of thing).  What do people think of this idea?

Re: Messing around with speedbonuses

Reply #1
The speedbonus does not depend on the waytype, but on the good. Look in pak64 svn for cars.xls. The first sheet is the speed bonus calaculation for rail vehicles (i.e. revenue at beginning at end per car and per ton). I tried to increase the revenue per ton constant at introduction and have them just break even at retirement.

Re: Messing around with speedbonuses

Reply #2
Thanks, I'll look at that.  But what about the speed settings in speedbonus.tab?  How do they affect the calculation?

Re: Messing around with speedbonuses

Reply #3
They set the speedbonus reference speed. The actual revenue is calculated in reference to those. (Without such entries, the reference speed will be the average speed of all powered vehicles of this waytype for the year).

Re: Messing around with speedbonuses

Reply #4
I think there may be a problem with the formula for calculating revenue in cars.xls - if you set maintenance to 0 and then choose a v_max well below the speedbonus speed, then according to the spreadsheet then you get negative income (!) - I have tested this in game using a horse (5kmh) and p****enger wagon in year 2000 (speedbonus speed 188kmh) and can generate positive revenue.  What is the actual formula used for calculating revenue in the game (****uming the formula in cars.xls is in fact incorrect).

Re: Messing around with speedbonuses

Reply #5
Sorry to drop this into the message flow..but if you are adjusting speed bonus gameplay could you consider this.

Is there any way to display the speed bonus/penalty?  For instance when a vehicle reaches its destination the amount earned appears on screen above the vehicle. Perhaps the bonus or penalty could be displayed eg. '135 (+10)'  or '135 (-5)'
Regards
Sev.

Re: Messing around with speedbonuses

Reply #6
@Severous - I'm not re-writing code, I'm just trying to get a good calculation of revenue for doing pak balancing.  But it would be a nice feature what you suggest.

@devs
I've also noticed that when you press "G" in game to get goods prices, the prices do not match the value quoted in the goods.dat (even for those where speedbonus = 0) - they seem to be all 1/3 of the value in goods.dat.  Is this a bug?

Re: Messing around with speedbonuses

Reply #7
I think there may be a problem with the formula for calculating revenue in cars.xls - if you set maintenance to 0 and then choose a v_max well below the speedbonus speed, then according to the spreadsheet then you get negative income (!)
That is the behaviour that I was told some time ago. It doesn't mean negative revenue, but a malus (which in extreme cases can lower revenue to nearly zero, but I guess not to negative values). I suggested to change it to bonus only (so you always get a base revenue).

Quote
- I have tested this in game using a horse (5kmh) and p****enger wagon in year 2000 (speedbonus speed 188kmh) and can generate positive revenue.  What is the actual formula used for calculating revenue in the game (****uming the formula in cars.xls is in fact incorrect).
If your observation is right (note: speedbonus is different between railroad and road vehicles!), I guess the program has changed its way of revenue calculation in this regard.

I've also noticed that when you press "G" in game to get goods prices, the prices do not match the value quoted in the goods.dat (even for those where speedbonus = 0) - they seem to be all 1/3 of the value in goods.dat.  Is this a bug?
No, it is for historic reasons, see http://simutrans-germany.com/wiki/wiki/tiki-index.php?page=en_GoodsDef.

Re: Messing around with speedbonuses

Reply #8
Thanks - helpful info on the goods value.  The formula I'm questioning is the one called "revenue" - I understand how speedbonus goes negative to reduce revenue, but this formula implies the actual revenue (i.e. base price + speedbounus) goes negative - take a look in cars.xls in pak64 SVN, play around with v_max and watch income go negative at small v_max...

And the horse test is correct - I was quoting speedbonus for rail and this was a rail vehicle I was using.

Re: Messing around with speedbonuses

Reply #9
Here is the (relevant part of the) code for calculating revenue in Simutrans-Standard:

Code: [Select]
if(  welt->get_einstellungen()->get_pay_for_total_distance_mode()==einstellungen_t::TO_DESTINATION  ) {
// pay only the distance, we get closer to our destination
while( iter.next() ) {

const ware_t & ware = iter.get_current();

if(  ware.menge==0  ) {
continue;
}

// now only use the real gain in difference for the revenue (may as well be negative!)
const koord &zwpos = ware.get_zielpos();
const long dist = koord_distance( zwpos, start ) - koord_distance( end, zwpos );

const sint32 grundwert128 = ware.get_besch()->get_preis()<<7; // bonus price will be always at least 0.128 of the real price
const sint32 grundwert_bonus = (ware.get_besch()->get_preis()*(1000+speed_base*ware.get_besch()->get_speed_bonus()));
const sint64 price = (sint64)(grundwert128>grundwert_bonus ? grundwert128 : grundwert_bonus) * (sint64)dist * (sint64)ware.menge;

// sum up new price
value += price;
}
}
else if(  welt->get_einstellungen()->get_pay_for_total_distance_mode()==einstellungen_t::TO_TRANSFER  ) {
// pay distance traveled to next trasnfer stop
while( iter.next() ) {

const ware_t & ware = iter.get_current();

if(ware.menge==0  ||  !ware.get_zwischenziel().is_bound()) {
continue;
}

// now only use the real gain in difference for the revenue (may as well be negative!)
const koord zwpos = ware.get_zwischenziel()->get_basis_pos();
const long dist = koord_distance( zwpos, start ) - koord_distance( end, zwpos );

const sint32 grundwert128 = ware.get_besch()->get_preis()<<7; // bonus price will be always at least 0.128 of the real price
const sint32 grundwert_bonus = (ware.get_besch()->get_preis()*(1000+speed_base*ware.get_besch()->get_speed_bonus()));
const sint64 price = (sint64)(grundwert128>grundwert_bonus ? grundwert128 : grundwert_bonus) * (sint64)dist * (sint64)ware.menge;

// sum up new price
value += price;
}
}
else {
// pay distance traveled
const long dist = koord_distance( start, end );
while( iter.next() ) {

const ware_t & ware = iter.get_current();

if(ware.menge==0  ||  !ware.get_zwischenziel().is_bound()) {
continue;
}

// now only use the real gain in difference for the revenue (may as well be negative!)
const sint32 grundwert128 = ware.get_besch()->get_preis()<<7; // bonus price will be always at least 0.128 of the real price
const sint32 grundwert_bonus = (ware.get_besch()->get_preis()*(1000+speed_base*ware.get_besch()->get_speed_bonus()));
const sint64 price = (sint64)(grundwert128>grundwert_bonus ? grundwert128 : grundwert_bonus) * (sint64)dist * (sint64)ware.menge;

// sum up new price
value += price;
}
}

In Simutrans-Experimental, it is different:

Code: [Select]

// Cannot not charge for journey if the journey distance is more than a certain proportion of the straight line distance.
// This eliminates the possibility of cheating by building circuitous routes, or the need to prevent that by always using
// the straight line distance, which makes the game difficult and unrealistic.
// If the origin has been deleted since the packet departed, then the best that we can do is guess by
// trebling the distance to the last stop.
const uint32 max_distance = ware.get_origin().is_bound() ? accurate_distance(ware.get_origin()->get_basis_pos(), fahr[0]->get_pos().get_2d()) * 2.2 : 3 * accurate_distance(last_stop_pos.get_2d(), fahr[0]->get_pos().get_2d());
const uint32 distance = ware.get_accumulated_distance();
const uint32 revenue_distance = distance < max_distance ? distance : max_distance;

ware.reset_accumulated_distance();

//Multiply by a factor (default: 0.3) to ensure that it fits the scale properly. Journey times can easily appear too long.
uint16 journey_minutes = (((float)distance / average_speed) * welt->get_einstellungen()->get_journey_time_multiplier() * 60);

const ware_besch_t* goods = ware.get_besch();
const uint16 price = goods->get_preis();
const sint32 min_price = price << 7;
const uint16 speed_bonus_rating = calc_adjusted_speed_bonus(goods->get_speed_bonus(), distance);
const sint32 ref_speed = welt->get_average_speed( fahr[0]->get_besch()->get_waytype() );
const sint32 speed_base = (100 * average_speed) / ref_speed - 100;
const sint32 base_bonus = (price * (1000 + speed_base * speed_bonus_rating));
const sint64 revenue = (sint64)(min_price > base_bonus ? min_price : base_bonus) * (sint64)revenue_distance * (sint64)ware.menge;
sint64 final_revenue = revenue;

const float happy_ratio = ware.get_origin().is_bound() ? ware.get_origin()->get_unhappy_proportion(1) : 1;
if(speed_bonus_rating > 0 && happy_ratio > 0)
{
// Reduce revenue if the origin stop is crowded, if speed is important for the cargo.
sint64 tmp = ((float)speed_bonus_rating / 100.0F) * revenue;
tmp *= (happy_ratio * 2);
final_revenue -= tmp;
}

if(ware.is_p****enger())
{
//P****engers care about their comfort
const uint8 tolerable_comfort = calc_tolerable_comfort(journey_minutes);
uint8 comfort = 100;
if(line.is_bound())
{
if(line->get_finance_history(1, LINE_COMFORT) < 1)
{
comfort = line->get_finance_history(0, LINE_COMFORT);
}
else
{
comfort = line->get_finance_history(1, LINE_COMFORT);
}
}
else
{
// No line - must use convoy
if(financial_history[1][CONVOI_COMFORT] < 1)
{
comfort = financial_history[0][CONVOI_COMFORT];
}
else
{
comfort = financial_history[1][CONVOI_COMFORT];
}
}

if(comfort > tolerable_comfort)
{
// Apply luxury bonus
const uint8 max_differential = welt->get_einstellungen()->get_max_luxury_bonus_differential();
const uint8 differential = comfort - tolerable_comfort;
const float multiplier = welt->get_einstellungen()->get_max_luxury_bonus();
if(differential >= max_differential)
{
final_revenue += (revenue * multiplier);
}
else
{
const float proportion = (float)differential / (float)max_differential;
final_revenue += revenue * (multiplier * proportion);
}
}
else if(comfort < tolerable_comfort)
{
// Apply discomfort penalty
const uint8 max_differential = welt->get_einstellungen()->get_max_discomfort_penalty_differential();
const uint8 differential = tolerable_comfort - comfort;
const float multiplier = welt->get_einstellungen()->get_max_discomfort_penalty();
if(differential >= max_differential)
{
final_revenue -= (revenue * multiplier);
}
else
{
const float proportion = (float)differential / (float)max_differential;
final_revenue -= revenue * (multiplier * proportion);
}
}

// Do nothing if comfort == tolerable_comfort
}

//Add catering or TPO revenue
const uint8 catering_level = get_catering_level(ware.get_besch()->get_catg_index());
if(catering_level > 0)
{
if(ware.is_mail())
{
// Mail
if(journey_minutes >= welt->get_einstellungen()->get_tpo_min_minutes())
{
final_revenue += (welt->get_einstellungen()->get_tpo_revenue() * ware.menge);
}
}
else if(ware.is_p****enger())
{
// P****engers
float proportion = 0.0F;
switch(catering_level)
{

case 1:
case_1:
if(journey_minutes < welt->get_einstellungen()->get_catering_min_minutes())
{
break;
}
if(journey_minutes > welt->get_einstellungen()->get_catering_level1_minutes())
{
final_revenue += (welt->get_einstellungen()->get_catering_level1_max_revenue() * ware.menge);
break;
}

proportion = (journey_minutes - welt->get_einstellungen()->get_catering_min_minutes()) / (welt->get_einstellungen()->get_catering_level1_minutes() - welt->get_einstellungen()->get_catering_min_minutes());
final_revenue += (proportion * (welt->get_einstellungen()->get_catering_level1_max_revenue() * ware.menge));
break;

case 2:
case_2:
if(journey_minutes < welt->get_einstellungen()->get_catering_level1_minutes())
{
// If only C++ had C#'s goto case syntax...
goto case_1;
}
if(journey_minutes > welt->get_einstellungen()->get_catering_level2_minutes())
{
final_revenue += (welt->get_einstellungen()->get_catering_level2_max_revenue() * ware.menge);
break;
}

proportion = (journey_minutes - welt->get_einstellungen()->get_catering_level1_max_revenue()) / (welt->get_einstellungen()->get_catering_level2_minutes() - welt->get_einstellungen()->get_catering_level1_minutes());
final_revenue += (proportion * (welt->get_einstellungen()->get_catering_level2_max_revenue() * ware.menge));
break;

case 3:
case_3:
if(journey_minutes < welt->get_einstellungen()->get_catering_level2_minutes())
{
goto case_2;
}

if(journey_minutes > welt->get_einstellungen()->get_catering_level3_minutes())
{
final_revenue += (welt->get_einstellungen()->get_catering_level3_max_revenue() * ware.menge);
break;
}

proportion = (journey_minutes - welt->get_einstellungen()->get_catering_level2_max_revenue()) / (welt->get_einstellungen()->get_catering_level3_minutes() - welt->get_einstellungen()->get_catering_level2_minutes());
final_revenue += (proportion * (welt->get_einstellungen()->get_catering_level3_max_revenue() * ware.menge));
break;

case 4:
case_4:
if(journey_minutes < welt->get_einstellungen()->get_catering_level3_minutes())
{
goto case_3;
}

if(journey_minutes > welt->get_einstellungen()->get_catering_level4_minutes())
{
final_revenue += (welt->get_einstellungen()->get_catering_level4_max_revenue() * ware.menge);
break;
}

proportion = (journey_minutes - welt->get_einstellungen()->get_catering_level3_max_revenue()) / (welt->get_einstellungen()->get_catering_level4_minutes() - welt->get_einstellungen()->get_catering_level3_minutes());
final_revenue += (proportion * (welt->get_einstellungen()->get_catering_level4_max_revenue() * ware.menge));
break;

case 5:
default:
if(journey_minutes < welt->get_einstellungen()->get_catering_level4_minutes())
{
goto case_4;
}

if(journey_minutes > welt->get_einstellungen()->get_catering_level5_minutes())
{
final_revenue += (welt->get_einstellungen()->get_catering_level5_max_revenue() * ware.menge);
break;
}

proportion = (journey_minutes - welt->get_einstellungen()->get_catering_level4_max_revenue()) / (welt->get_einstellungen()->get_catering_level5_minutes() - welt->get_einstellungen()->get_catering_level4_minutes());
final_revenue += (proportion * (welt->get_einstellungen()->get_catering_level5_max_revenue() * ware.menge));
break;
};
}
}
Download Simutrans-Extended.

Want to help with development? See here for things to do for coding, and here for information on how to make graphics/objects.

Follow Simutrans-Extended on Facebook.

Re: Messing around with speedbonuses

Reply #10
in pak64 svn there is an excel-file, which can calculate maximum speed and maximum revenue for any vehicle at intro and retire year (given the relevant average speeds in those years).

Re: Messing around with speedbonuses

Reply #11
in pak64 svn there is an excel-file, which can calculate maximum speed and maximum revenue for any vehicle at intro and retire year (given the relevant average speeds in those years).
The Hood got the results from that file.
(If I understood the source correctly:) The formula in the .XLS is incomplete and therefore wrong, because it doesn't take into account the minimum income, which is 1/8 of the base revenue (which matches to 0% bonus).

Re: Messing around with speedbonuses

Reply #12
That is exactly what I am saying.  The formula in the xls generates negative incomes even with 0 maintenance per tile.

@whoami, are you saying that if I add 1/8 of the base revenue, this will fix the formula? c

Edit: or is it that 1/8 of base price is the lowest possible income, and if the speedbonus calculated in the xls formula theoretically makes the income negative, then 1/8 of base price is used?  This seems to be consistent with the behaviour of the goods price dialogue in the game.