Pebbles in the Flood - Can hold out to infinity

Having trouble with the game? Report issues and get help here. Read this first!

Moderators: Forum Moderators, Developers

Forum rules
Before reporting issues in this section, you must read the following topic:
tomm
Posts: 2
Joined: September 21st, 2014, 9:13 am

Pebbles in the Flood - Can hold out to infinity

Post by tomm » September 21st, 2014, 9:45 am

Sir Gerrick is just 1 XP short of level up and no one wants to attack him, unless I attack first. Since this is The South Guard - Pebbles in the Flood scenario, where you need to hold out for as long as possible, I can do that to infinity.

OS: Debian 7 (Wheezy)
Wesnoth 1.10.3 - English (US) (from the Debian repo)


Turn 33:
Image

Turn 104:
Image
Attachments
TSG-Pebbles_in_the_Flood-Auto-Save107.gz
(119.78 KiB) Downloaded 168 times
TSG-Pebbles_in_the_Flood-Auto-Save1.gz
(31.9 KiB) Downloaded 161 times

User avatar
Ravana
Moderator
Posts: 2155
Joined: January 29th, 2012, 12:49 am
Location: Estonia
Contact:

Re: Pebbles in the Flood - Can hold out to infinity

Post by Ravana » September 21st, 2014, 10:34 am

This is normal ai behaviour, so not bug, scenario might need change.

User avatar
beetlenaut
Developer
Posts: 2361
Joined: December 8th, 2007, 3:21 am
Location: Washington State
Contact:

Re: Pebbles in the Flood - Can hold out to infinity

Post by beetlenaut » September 21st, 2014, 5:51 pm

This shouldn't be normal behavior, so I think that should change, not this scenario. You don't normally want to cause a unit to upgrade, but sometimes you have to.

In this scenario, there is no benefit to holding out longer than 22 turns, so go ahead and attack.
Campaigns: Dead Water,
The Founding of Borstep,
Secrets of the Ancients,
and WML Guide

tomm
Posts: 2
Joined: September 21st, 2014, 9:13 am

Re: Pebbles in the Flood - Can hold out to infinity

Post by tomm » September 21st, 2014, 6:07 pm

I've already completed it.

I found it amusing that they didn't attack, so i was going "just one more turn". :)

mattsc
Posts: 1103
Joined: October 13th, 2010, 6:14 pm
Location: Wandering, mostly aimlessly

Re: Pebbles in the Flood - Can hold out to infinity

Post by mattsc » September 21st, 2014, 6:59 pm

beetlenaut wrote:This shouldn't be normal behavior, so I think that should change, not this scenario. You don't normally want to cause a unit to upgrade, but sometimes you have to.
I agree, in principle, but the devil is in the details. In fact, it is already like this. Sometimes the AI does attack a unit 1 XP from leveling. It's just that the condition is set in such a way (attack only if there's a chance, however small, to kill the enemy unit before it levels) that it is easily exploitable. For anything that involves a "usually you shouldn't but sometimes you should" decision, there will always be situations that can be exploited by a human player who's sufficiently experienced against the Wesnoth AI and it is up to the scenario designer to prevent that. In fact, when we rebalanced Son of the Black Eye, we did that in a number of scenarios, see for example here and here. But that is, of course, not a solution that the AI should use as a general rule.

I don't want to say that this particular behavior should not be changed. In fact, I really think that it should (to the point that, when I am playing a campaign, I do not make use of this particular "feature"), I just don't know how. This is not as easy as it sounds, if you want to do in a way that does not result in something that is just as easily, or more so, exploitable in a different way.

And I have spent quite a bit of time thinking about this and playing around with it in my own AI experiments, so anybody who wants to convince me otherwise better come up with a very concrete suggestion rather than some generalisms. ;)

User avatar
beetlenaut
Developer
Posts: 2361
Joined: December 8th, 2007, 3:21 am
Location: Washington State
Contact:

Re: Pebbles in the Flood - Can hold out to infinity

Post by beetlenaut » September 21st, 2014, 7:47 pm

I'm not sure I stand by my own previous statement any more, at least for the AI. Maybe the solution is just to make the AI prefer to attack units one or two points from upgrading. It seems like it should be a high-priority action so it happens early in the turn. I would also add these conditions, at least as a first guess: the AI must have at least one other unit that can reach the newly-upgraded one this turn; the AI shouldn't use poison, slows, magic, or marksman for this; and the AI must have at least four total units remaining. If the high-XP unit could possibly be killed, that attack should still take priority.

While this would be exploitable, I usually wouldn't want to exploit it.
Campaigns: Dead Water,
The Founding of Borstep,
Secrets of the Ancients,
and WML Guide

mattsc
Posts: 1103
Joined: October 13th, 2010, 6:14 pm
Location: Wandering, mostly aimlessly

Re: Pebbles in the Flood - Can hold out to infinity

Post by mattsc » September 22nd, 2014, 12:58 am

beetlenaut wrote:Maybe the solution is just to make the AI prefer to attack units one or two points from upgrading. It seems like it should be a high-priority action so it happens early in the turn. I would also add these conditions, at least as a first guess: the AI must have at least one other unit that can reach the newly-upgraded one this turn; the AI shouldn't use poison, slows, magic, or marksman for this; and the AI must have at least four total units remaining. If the high-XP unit could possibly be killed, that attack should still take priority.
Thanks, that sounds like a good set of conditions to start out with. It's also reasonably easy to code that, or at least an approximation of it, as a Lua AI, so why don't I do that and people can test it for a little and send comments and suggestions for improvements.

Well, we can't add this to the default AI in the 1.11/1.12 branch anyway, since it would break the feature freeze and potentially unbalance some scenarios. But we could add a custom AI to Pebbles in the Flood, as that scenario is pretty much broken under the conditions reported by tomm.

User avatar
Pentarctagon
Forum Administrator
Posts: 3945
Joined: March 22nd, 2009, 10:50 pm
Location: Earth (occasionally)

Re: Pebbles in the Flood - Can hold out to infinity

Post by Pentarctagon » September 22nd, 2014, 1:13 am

Would it be a good idea to have some kind of failsafe? So for example, if attacking the unit that's about to level up is the only possible move, just attack it with anything after X turns.
99 little bugs in the code, 99 little bugs
take one down, patch it around
-2,147,483,648 little bugs in the code

User avatar
beetlenaut
Developer
Posts: 2361
Joined: December 8th, 2007, 3:21 am
Location: Washington State
Contact:

Re: Pebbles in the Flood - Can hold out to infinity

Post by beetlenaut » September 22nd, 2014, 2:21 am

Pentarctagon wrote:just attack it with anything after X turns.
The simple attack micro AI does exactly that, but I suspect that switching to it by default might have some unintended consequences. Leaving it up to the scenario designer to use is probably a better option. If you are just talking about fixing Pebbles in the Flood though, that would do it.
Campaigns: Dead Water,
The Founding of Borstep,
Secrets of the Ancients,
and WML Guide

mattsc
Posts: 1103
Joined: October 13th, 2010, 6:14 pm
Location: Wandering, mostly aimlessly

Re: Pebbles in the Flood - Can hold out to infinity

Post by mattsc » September 24th, 2014, 4:56 pm

I put together a first attempt at an attack high XP enemy units AI that does pretty close to what beetlenaut suggested above. It still has some caveats (see below), but I think it is ready for some testing.
How it works
It kicks in before the default AI's attacks if:
  1. There is an enemy unit within 3XP of leveling and if that unit cannot be attacked by a unit that would not cause it to level up. In the latter case, we leave the attack to the RCA AI.
  2. The AI has at least 4 units total (modifiable parameter)
  3. The AI has at least 2 units (modifiable parameter) that can attack the enemy unit from different hexes. That is, it must be possible to follow up with at least one more attack after the one causing the leveling.
The AI rates all possible attacks as follows:
  1. Attacks that might result in a kill are always preferred over no-kill-chance attacks, with the highest CTK attack being executed
  2. Otherwise, we prefer the attack that would do the least damage to the enemy (if no level up were to occur). That means that the stronger attacks can be used afterward on the leveled enemy.
  3. If several attacks are equal in the previous criterion, we prefer the one for which the attacking AI unit receives the least damage.
  4. Slightly different from beetlenaut's suggestion: poison and slow attacks might be used, but are strongly discouraged. By contrast, marksman and magical attacks are not given special consideration, but they are unlikely to be used based on the least-damage attack being preferred.
After the level-up attack is done, the rest of the attacks are left to the default AI.
Known Problems
  1. The main problem at the moment is how to decide if a unit should be attacked. Let's say we have an enemy unit 2XP from leveling and there are L1 and L2 units that could attack it. The current code does not attack in that case, as the default AI might choose one of the L1s to attack. After that, the unit is 1XP from leveling, and the custom AI would kick in. Sounds great, in princple, but the problem is that the default AI does not always attack with an L1 (unless aggression=1 is set). I'm not entirely sure how to deal with this. The easiest would be always to force an attack if one of the units could cause a leveling of the enemy. But I am not sure that that is ideal. Suggestions welcome ...
  2. If there are more than one enemy units close to leveling, the level-up attack(s) might not be executed in some cases. That's easy to take care of, I just haven't had time for it yet.
  3. The code's not optimized for speed yet. I don't think that's a big problem (as in, it should still be reasonably fast), so I won't do that until the very end.
So what would be the best way to make this available to people for testing? I can easily turn this into a Micro AI, but I can only do that for Wesnoth 1.13 (because of the feature freeze in 1.12) and there's no 1.13 release out yet. In 1.12, it can be put into scenarios quite easily using the instructions below. Maybe that's good enough for now?
How to use the AI in a scenario (requires 1.11.16 or later)
Put the following code into a file called ca_attack_highxp.lua anywhere you want:

Code: Select all

local AH = wesnoth.require "ai/lua/ai_helper.lua"
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
local LS = wesnoth.require "lua/location_set.lua"

local min_units = 4
local min_attackers = 2

local ca_attack_highxp = {}

function ca_attack_highxp:evaluation(ai, cfg, self)
    -- Units with attacks left
    local attackers = AH.get_units_with_attacks { side = wesnoth.current.side }
    if (not attackers[1]) then return 0 end

    -- AI needs at least @min_units units total
    local units = wesnoth.get_units { side = wesnoth.current.side }
    if (#units < min_units) then return 0 end

    -- Is there an enemy within 3 XP of leveling up?
    local target
    local enemies = wesnoth.get_units {
        { "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
    }

    for _,enemy in ipairs(enemies) do
        if ((enemy.experience + 3) >= enemy.max_experience) then
            target = enemy
            break
        end
    end

    if (not target) then return 0 end

    -- All possible attacks
    local attacks = AH.get_attacks(attackers, { include_occupied = true })

    -- Only attack the target if at least two AI units can attack it
    local src_map, dst_map = LS.create(), LS.create()
    for i=#attacks,1,-1 do
        if (attacks[i].target.x == target.x) and (attacks[i].target.y == target.y) then
            -- If there is an attack that would NOT result in leveling up,
            -- do not execute any attack, as the default AI will take care of this
            local attacker = wesnoth.get_unit(attacks[i].src.x, attacks[i].src.y)
            if (attacker.__cfg.level < target.max_experience - target.experience) then
                return 0
            end

            src_map:insert(attacks[i].src.x, attacks[i].src.y)
            dst_map:insert(attacks[i].dst.x, attacks[i].dst.y)
        else
            table.remove(attacks, i)
        end
    end
    if (src_map:size() < min_attackers) or (dst_map:size() < min_attackers) then
        return 0
    end

    -- Rate all the attacks
    local max_rating, best_attack = -9e99
    for _,att in ipairs(attacks) do
        local rating
        local attacker = wesnoth.get_unit(att.src.x, att.src.y)

        local attacker_copy = wesnoth.copy_unit(attacker)
        attacker_copy.x, attacker_copy.y = att.dst.x, att.dst.y

        local att_stats, def_stats, att_weapon, def_weapon = wesnoth.simulate_combat(attacker_copy, target)

        if (def_stats.hp_chance[0] > 0) then
            -- If a kill is possible, strongly prefer that attack
            rating = 1000 + def_stats.hp_chance[0]
        else
            -- Otherwise choose the attacker that would do the *least*
            -- damage if the target were not to level up
            local old_experience = target.experience
            target.experience = 0
            local att_stats, def_stats, att_weapon, def_weapon = wesnoth.simulate_combat(attacker_copy, target)
            target.experience = old_experience

            rating = def_stats.average_hp

            -- Damage taken by the AI unit is a lesser rating contribution
            local own_damage = attacker.hitpoints - att_stats.average_hp
            rating = rating - own_damage / 100.

            -- Strongly discourage poison or slow attacks
            if att_weapon.poisons or att_weapon.slows then
                rating = rating - 100
            end

            -- Minor penalty if the attack hex is occupied
            if att.attack_hex_occupied then
                rating = rating - 0.001
            end
        end

        if (rating > max_rating) then
            max_rating, best_attack = rating, att
        end
    end

    if best_attack then
        self.data.XP_attack = best_attack
        return 100010
    end

    return 0
end

function ca_attack_highxp:execution(ai, cfg, self)
    local attacker = wesnoth.get_unit(self.data.XP_attack.src.x, self.data.XP_attack.src.y)
    local defender = wesnoth.get_unit(self.data.XP_attack.target.x, self.data.XP_attack.target.y)

    AH.movefull_outofway_stopunit(ai, attacker, self.data.XP_attack.dst.x, self.data.XP_attack.dst.y)
    if (not attacker) or (not attacker.valid) then return end
    if (not defender) or (not defender.valid) then return end

    AH.checked_attack(ai, attacker, defender)
    self.data.XP_attack = nil
end

return ca_attack_highxp
Then put the following into the [side] tag for the side that is supposed to use this AI and adjust the path to match wherever you have put the code in the previous step. (In principle you should also change the '2' to the side number, but it works even if you don't.)

Code: Select all

        [ai]
            {MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop (
                [candidate_action]
                    engine=lua
                    name=attack_highca
                    id=attack_highca
                    max_score=100010
                    location="~add-ons/AI-demos/lua/ca_attack_highxp.lua"
                [/candidate_action]
            )}
        [/ai]

User avatar
doofus-01
Art Contributor
Posts: 3768
Joined: January 6th, 2008, 9:27 pm
Location: USA, the civilized part.

Re: Pebbles in the Flood - Can hold out to infinity

Post by doofus-01 » September 25th, 2014, 1:53 am

mattsc wrote:The main problem at the moment is how to decide if a unit should be attacked. Let's say we have an enemy unit 2XP from leveling and there are L1 and L2 units that could attack it. The current code does not attack in that case, as the default AI might choose one of the L1s to attack. After that, the unit is 1XP from leveling, and the custom AI would kick in. Sounds great, in princple, but the problem is that the default AI does not always attack with an L1 (unless aggression=1 is set). I'm not entirely sure how to deal with this. The easiest would be always to force an attack if one of the units could cause a leveling of the enemy. But I am not sure that that is ideal. Suggestions welcome ...
Would it be practical to:
- save the id of the 2XP-until-it-levels unit,
- then retain it as a filter for the next turn, so if the same unit is still 2XP-until-level,
- the custom AI condition #1 is overridden, and an attack is forced?

It won't help the AI be wiser within one turn, but it could possibly prevent (or at least complicate) multi-turn exploitation.
BfW 1.12 supported, but active development only for BfW 1.13/1.14: Bad Moon Rising | Trinity | Archaic Era |
| Abandoned: Tales of the Setting Sun
GitHub link for these projects

User avatar
tekelili
Posts: 1038
Joined: August 19th, 2009, 9:28 pm

Re: Pebbles in the Flood - Can hold out to infinity

Post by tekelili » September 25th, 2014, 3:38 am

mattsc wrote:The main problem at the moment is how to decide if a unit should be attacked.
I always thought that when a "none solution is good" situation happens in AI behavior, it should be solved with some randomess in its behavior. That would at least solve "easy exploits from players" wich is at the end main problem, imho.

edit: Or saying it in other way: If you reach conclusion any algorithm will have AI behaving stupidly in some cases, then dont let player predict those cases.
Be aware English is not my first language and I could have explained bad myself using wrong or just invented words.
World Conquest II

mattsc
Posts: 1103
Joined: October 13th, 2010, 6:14 pm
Location: Wandering, mostly aimlessly

Re: Pebbles in the Flood - Can hold out to infinity

Post by mattsc » September 26th, 2014, 3:31 am

doofus-01: That is certainly feasible and might be a good fall-back option if we cannot come up with a reliable solution for the current turn.

tekelili: I read somewhere that the AI should not be basing its decisions on randomness, but in all the discussions I've had with other Wesnoth players, we've agreed that unpredictability is part of the fun. So, yes, that's also something to consider.

I'd still like to try if we can come up with some sort of reasoning for making a more informed decision first, and using your suggestions as a fall-back for when that does not work. In the case described, the default AI only attacks when it either has a chance for a kill, or when it can attack at a range at which the enemy cannot strike back. It should be (relatively) easy in most cases to figure out when that is the case and force the attack otherwise - maybe with some randomness built in and definitely attacking on the next turn if nothing happened this turn ...

Thanks for the suggestions! I'll keep thinking about this.

User avatar
tekelili
Posts: 1038
Joined: August 19th, 2009, 9:28 pm

Re: Pebbles in the Flood - Can hold out to infinity

Post by tekelili » April 9th, 2015, 9:18 am

I have been recently abusing a lot of almost leveled units over AI in World Conquest. In this campaign fronts are quite large, and I almost never got a "total lock". From what I most profited, was when I needed close front line in a spot with low defense like grass. As the AI often refused attack such unit, I could repeat several times trick, keeping front lines that otherwise could need a retreat. After evaluating how much would have damaged my army an early attack to level such units, and how could be that "understood" by aI, I reached to an idea it could worth try:

I never read BfW AI code, but I suspect damage/retaliation/chance to kill are the most important factors. If I am right with that, I propose AI candidate action to attack a unit with 0% chance to kill and would level up, becomes evaluated in function of dealing half expected damage (without heal from leveling considered) and double retaliation taken.

I reached to that conclusion, after realize in fact AI has to do 2 attacks to damage.

edit: Take 50%-200% values just as idea, AI could need different ones to evalaute well situation. What my idea means is that an intellegent AI should understand it is stupid :lol: If I were an AI I would think "those pesky humans minds almost ever outplay me, but from past experiences: they look to outplay me harder when I use to avoid attack units to level up than when I am prone to attack such units soon"
Be aware English is not my first language and I could have explained bad myself using wrong or just invented words.
World Conquest II

mattsc
Posts: 1103
Joined: October 13th, 2010, 6:14 pm
Location: Wandering, mostly aimlessly

Re: Pebbles in the Flood - Can hold out to infinity

Post by mattsc » April 10th, 2015, 11:56 pm

tekelili: you do indeed understand the main considerations of the AI for rating an attack, and deciding whether an attack is worth it. However, if I interpret what you are writing correctly, there's a problem with the practical implementation. If I understand you correctly, you are suggesting to evaluate an attack the way you suggest (which sounds perfectly reasonable), and then compare it to other attacks as evaluated by the standard combat candidate action.

And that comparison is the issue, because while we do have access to the result of the default evaluation function via ai.get_attacks(), we cannot pass a different (additional, whatever) function or parameter set to it without modifying the C++ code. So what we would need to do is write a Lua function that replicates the default combat rating function in Lua, so that we can substitute parts or inputs as needed. That's not really a problem per se, it just requires somebody to do so. In fact, I think I had most of that written at some point. I should search my computer sometime to see if I still have that code somewhere ...

All of that is, of course, only correct/applicable/whatever if I understand what you are saying correctly.

Post Reply