关于编写干净的代码……用于战斗

因此,我们之前已经讨论了干净代码的重要性。 那篇文章表明,经济交流如何清除克隆过程中涉及的其他混乱步骤。 但是,如果您的麻烦大于一步,会发生什么? —在我们的新技术博客文章中,我们邀请您加入我们的(代码)战斗……

在Tau站,当您开始战斗时,您可能会认为战斗不顺利,因此尝试逃离。 逻辑很简单。

  1. 表示我们想逃离
  2. 对手获得自由进攻
  3. 尝试逃到车站内的随机区域
  4. 结束战斗

这很简单。 您可能会注意到,许多步骤与我们可能在代码的其他地方(攻击,位置更改,结束战斗)可能使用的步骤非常相似。 在我们的原始代码中,我们有如下内容:

sub flee_combat ($self) { 
my $combat = $self->in_combat or return 1;
my $defender = $combat->defender;
my $result = $defender->_attack($self);
if ( !$result ) {
# combat was ended by death or confinement
$self->add_message({
message => "You were unable to flee the combat.",
message_type => 'combat-attacker',
});
$self->log_event(
'Combat',
type => 'attacker_flee',
success => 0,
character => $self,
victim => $defender->name,
reason => "attacker died on defender's bonus attack"
);
return 1;
}
my $chance = $self->_chance_to_run_away($defender);
if ( $self->attempt_to('combat-flee', $chance) ) {
$self->change_station_area( $self->_random_area );
if ( 'security' eq $self->area->slug ) {
$self->_ran_into_security_fleeing_combat($defender);
$defender->add_message({
message => [
"%s fled from combat but was caught by security.",
$self->name
],
message_type => 'combat-defender',
});
$self->log_event(
'Combat',
type => 'attacker_flee',
success => 1,
character => $self,
victim => $defender->name,
reason => "attacker fled straight into security"
);
}
else {
$self->add_message({
message => "You got away!",
message_type => 'combat-attacker',
});
$defender->add_message(
{ message => [ "%s fled from combat", $self->name ] },
message_type => 'combat-defender',
);
$self->log_event(
'Combat',
type => 'attacker_flee',
success => 1,
character => $self,
victim => $defender->name
);
}
$combat->delete;
return 1;
}
else {
$self->add_message(
{ message => "You were too slow to get away!" },
message_type => 'combat-attacker',
);
$self->log_event(
'Combat',
type => 'attacker_flee',
success => 0,
character => $self,
victim => $defender->name,
reason => "attacker was too slow to get away"
);
}
return;
}

这具有我们在上一篇文章中提到的有关清理代码的所有问题。 太杂乱了,做了很多交叉的事情。

我们可以利用经济交流来对此进行一些清理。 我们将所有内容隔离为步骤,并针对每场战斗进行检查,然后,只有在一切成功之后,我们才将更改写入数据库。 最终看起来像这样:

 sub flee_combat ($self) { 
my $combat = $self->in_combat or return 1;
my $defender = $combat->defender;
my $exchange = $self->new_exchange(
slug => $slug,
Steps(
Precondition( $self => allow_when => 'in_combat' ),
Combat( $combat => 'is_active' ),
FAILURE(
Location( $self => 'send_to_brig_for_combat_timeout' )
),
Combat( $combat => 'rounds_left' ),
FAILURE(
Location( $self => 'send_to_brig' )
),
Combat( $combat => 'start_round' ),
my $exchange = $self->new_exchange(
slug => $slug,
Steps(
Precondition( $self => allow_when => 'in_combat' ),
Combat( $combat => 'is_active' ),
FAILURE(
Location( $self => 'send_to_brig_for_combat_timeout' )
),
Combat( $combat => 'rounds_left' ),
FAILURE(
Location( $self => 'send_to_brig' )
),
Combat( $combat => 'start_round' ),
# give the defender a chance to attack us when we flee
Combat(
$combat => 'round' => {
actor => $self->as_combatant,
target => $defender->as_combatant,
actions => [
Veure::Combat::Action::Attack->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
%$args,
),
# give the defender a chance to attack us when we flee
Combat(
$combat => 'round' => {
actor => $self->as_combatant,
target => $defender->as_combatant,
actions => [
Veure::Combat::Action::Attack->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
%$args,
),
# give the defender a chance to attack us when we flee
Combat(
$combat => 'round' => {
actor => $self->as_combatant,
target => $defender->as_combatant,
actions => [
Veure::Combat::Action::Attack->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
%$args,
),
# Now we give the defender a chance to flee.
# If their stat levels have dropped too low to stay
# in combat the target becomes the "actor" of
# the Flee step
Veure::Combat::Action::Flee->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
when => sub ($self) {
my $character = $self->actor;
my $too_low = $character->stats_too_low_in_combat;
return $too_low;
},
%$args,
);
# Now we give the defender a chance to flee.
# If their stat levels have dropped too low to stay
# in combat the target becomes the "actor" of
# the Flee step
Veure::Combat::Action::Flee->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
when => sub ($self) {
my $character = $self->actor;
my $too_low = $character->stats_too_low_in_combat;
return $too_low;
},
%$args,
);
# now we *actually* attempt to flee
Veure::Combat::Action::Flee->new(
actor => $self->as_combatant,
target => $defender->as_combatant,
when => sub {1},
%$args,
);
],
}
),
# now we *actually* attempt to flee
Veure::Combat::Action::Flee->new(
actor => $self->as_combatant,
target => $defender->as_combatant,
when => sub {1},
%$args,
);
],
}
),
# now save all the changes that happened in
# the Combat Round to the database
Stats(
$self => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$self->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $self => flee_to => "combat_round.${who}_fled_to" ),
# now save all the changes that happened in
# the Combat Round to the database
Stats(
$self => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$self->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $self => flee_to => "combat_round.${who}_fled_to" ),
# now save all the changes that happened in
# the Combat Round to the database
Stats(
$self => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$self->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $self => flee_to => "combat_round.${who}_fled_to" ),
# and do the same for the defender
Stats(
$defender => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$defender->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $defender => flee_to => "combat_round.${who}_fled_to" ),
ALWAYS( Combat( $combat => 'end_round' ) ),
)
);
}
# and do the same for the defender
Stats(
$defender => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$defender->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $defender => flee_to => "combat_round.${who}_fled_to" ),
ALWAYS( Combat( $combat => 'end_round' ) ),
)
);
}

更好,但是仍然不是很好。 我们在那里有很多样板,因为战斗中的每一步都要经过很多检查。 我们仍然有很多重复的代码,因为我们要为攻击者执行的操作我们也想为防御者执行。 这是更高阶编程的地方。

Mark Jason Dominus在他的Perl Perl高阶书中指出:

高阶Perl与Perl中的函数式编程技术有关。 关于如何编写可以修改和制造其他功能的功能。

我们在经济交易中已经使用Combat()类的东西进行了一些函数式编程,这些函数只是导出经济交易期望进行的数据结构的函数。 那么我们如何在这里应用这个想法? 让我们从一些基本的事情开始,例如清理那些结果步骤:

 sub CombatOutcome ( $who, $character ) { 
return (
Stats(
$character => remove_points => {
slug => "combat_round.${who}_damage",
force_to_zero => 1,
}
),
Inventory(
$character->inventory => damage_items =>
"combat_round.${who}_item_damage"
),
Location( $character => flee_to => "combat_round.${who}_fled_to" ),
);
}

这将涉及战斗结果的20条线更改为:

 Combat( $combat => 'round' => { ... } ), 
CombatOutcome( target => $self ),
CombatOutcome( actor => $defender );
ALWAYS( Combat( $combat => 'end_round' ) ),

接下来,如何逃离。 我们在上面做了两次(一次响应战斗情况,一次是我们在战斗回合中要实现的主要目标)。 如果我们把这两个步骤都拉出来,我们可以有一个这样的函数:

 sub Flee ( $actor, $target, %args) { 
Veure::Combat::Action::Flee->new(
actor => $actor->as_combatant,
target => $target->as_combatant,
when => sub { 1 },
%args,
);
}

然后,我们将得到如下结果:

 Combat( 
$combat => 'round' => {
actor => $self->as_combatant,
target => $defender->as_combatant,
actions => [
Veure::Combat::Action::Attack->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
%$args,
),
Combat(
$combat => 'round' => {
actor => $self->as_combatant,
target => $defender->as_combatant,
actions => [
Veure::Combat::Action::Attack->new(
actor => $defender->as_combatant,
target => $self->as_combatant,
%$args,
),
# now we give the defender a chance to flee.
# If their stat levels have dropped too low to stay in combat
# the target becomes the "actor" of the Flee step
Flee(
$defender => $self,
when => sub ($self) {
my $character = $self->actor;
my $too_low = $character->stats_too_low_in_combat;
return $too_low;
}
);
# now we give the defender a chance to flee.
# If their stat levels have dropped too low to stay in combat
# the target becomes the "actor" of the Flee step
Flee(
$defender => $self,
when => sub ($self) {
my $character = $self->actor;
my $too_low = $character->stats_too_low_in_combat;
return $too_low;
}
);
# now we *actually* attempt to flee
Flee($self => $defender)
],
}
),

如果我们继续这样做,则为Attack创建函数(其中包括逃跑检查),甚至为CombatRound创建完整的交换创建,我们最终将获得一个包含逻辑核心部分的函数:

 sub flee_combat ( $self ) { 
my $combat = $self->current_combat // return;
my $defender = $self->combat_opponent;
my $exchange = CombatRound(
$combat => (
Attack( $defender => $self ),
Flee( $self => $defender ),
Combat( $combat => 'end_combat' ),
)
);
return $exchange->stash->get('combat_round.fled');
}

这是很容易阅读和遵循的,并且假设每个步骤都完全符合我们的想法,更容易确保基础逻辑正确。 这是我们真正希望的。


最初于 2018 年2月8日 发布在 taustation.space 上。