Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FirstToScore cancels ongoing action when different action passes threshold #73

Open
ZilvinasAbr opened this issue Mar 13, 2023 · 8 comments

Comments

@ZilvinasAbr
Copy link

I've found out that FirstToScore behaves not how I thought it should.

Here's a code snippet:

        // Define a multistep action of moving and drinking
        let move_and_drink = Steps::build()
            .label("MoveAndDrink")
            .step(MoveToWaterSource)
            .step(Drink);

        // Define a multistep action of moving and eating
        let move_and_eat = Steps::build()
            .label("MoveAndEat")
            .step(MoveToFoodSource)
            .step(Eat);

        // Define the thinker
        let thinker = Thinker::build()
            .label("Thinker")
            .picker(FirstToScore { threshold: 0.8 })
            .when(Hungry, move_and_eat)
            .when(Thirsty, move_and_drink);

Situation:

  1. Thirsty is 0.6, Hungry is 0.8
  2. Thinker starts executing move and eat action.
  3. Meanwhile doing the MoveToFoodSource part of the action, Thirsty reaches the threshold of 0.8.

Expected Behavior: Since the score calculation is FirstToScore, the move_and_eat action is continuing execution because it reached the threshold first.

Actual Behavior: MoveToFoodSource gets cancelled and MoveAndDrink starts executing.

In this exact case it this happens because move_and_eat is defined first in the thinker and move_and_drink second. It prioritizes the firstly defined action.

@zkat
Copy link
Owner

zkat commented Mar 13, 2023

This is working as intended. FirstToScore means "every cycle, check from top to bottom, and pick the first item that scores above the threshold". If that item changes between cycles, the cancellation will happen. The picker is about which action is picked on every individual test, not between action runs.

@ZilvinasAbr
Copy link
Author

Got it, thanks for clarifying that! I wonder how it would be possible to implement it so that it would behave as I'd like? Should I try to implement a custom Picker that would achieve that? Or is there a better/simpler way?

@zkat
Copy link
Owner

zkat commented Mar 14, 2023

@ZilvinasAbr unless I'm missing something, you shouldn't need to implement a custom picker or anything: move_and_eat should continue executing until you set the action state to either ActionState::Success or ActionState::Failure. You will receive ActionState::Cancelled when the thinker cancels the action, but the "next" action won't be executed until you've completed the current one, so it's your choice when the next one actually starts. Would that work?

@ZilvinasAbr
Copy link
Author

Ok, so I tried to do this:

ActionState::Executing | ActionState::Cancelled => {

for all the actions (eat food,drink water and go to food/water). Essentially, finishing the current action even if thinker tries to cancel it. But now the issue is that I don't know how to enforce that a multi step action would be completed before moving to do another multi step action.
E.g.

  1. thirst: 0.4, hunger: 0.8
  2. go to food action executing
  3. while it is executing, thirst reaches 0.8
  4. Thinker tries to cancel go to food action, but we are still trying to complete it before cancelling.
  5. Human reaches food, but the step eat food does not get executed because the whole multistep action got cancelled.
  6. go to water action starts execution.

@zkat
Copy link
Owner

zkat commented Mar 19, 2023

You should take ActionState::Cancelled as a sign that some cancellation has been requested and act accordingly, instead of continuing as if it never happened. It means "wrap up your work". If you treat it that way, it should behave as you want it to, I think, in the bigger picture.

@gak
Copy link

gak commented Apr 7, 2023

I think I'm having a similar issue as @ZilvinasAbr and apologies if I don't understand the internals properly.

There is a set of Action steps that shouldn't be cancelled, and FirstToScore is cancelling it. I want the whole Steps to complete before a new action is evaluated.

Maybe another option is to somehow ignore the CancelledState and be able to complete the Action and the steps afterwards. e.g. ActionState::Cancelled => *state = ActionState::Continue

My use case is a little ant colony:

AntType::Scout => Thinker::build()
    .label("ScoutThinker")
    .picker(FirstToScore { threshold: 0.5 })
    .when(HungryScorer, eat_food())
    .otherwise(discover_food_and_offer_to_the_queen_steps()),
pub fn eat_food() -> StepsBuilder {
    Steps::build()
        .label("Eat")
        .step(SetPathToStoredFoodAction)
        .step(PathfindingAction)
        .step(EatAction::default())
}

pub fn discover_food_and_offer_to_the_queen_steps() -> StepsBuilder {
    Steps::build()
        .label("DiscoverFood")
        .step(SetPathToRandomOutsideAction)
        .step(PathfindingAction)
        .step(MapTransitionAction::exit())
        .step(OutsideMapDiscoveringNewFoodAction::default())
        .step(MapTransitionAction::enter())
        .step(SetPathToQueenAction)
        .step(PathfindingAction)
        .step(OfferFoodDiscoveryToQueenAction)
}

For example, when the DiscoverFood Steps is cancelled half way because of HungryScorer, I couldn't find a way to complete all the steps.

@zkat
Copy link
Owner

zkat commented Apr 9, 2023

I think your best bet for this one is to create a Steps that forces completion of all the steps, but this whole thing pushes a little against the default model of "you will only continue doing things as long as they still make sense to you"

@JtotheThree
Copy link

I ran into the same confusion. I have two scorers that are multi step with Drink being one of the actions. If the drink quenches thirst over multiple frames then Drink Until thirst = x doesn’t work because another scorer comes in higher so the actor stops drinking. Then thirst quickly scores higher again and it goes back to drinking quickly.

I think some of the confusion is the example “drink until” gives people the impression the actor will do nothing else until it’s drunk enough to reach the desired threshold.

I assume the way to handle this is in the scorers to add some logic to the thirst scorer. Score thirst low until it hits say 0.8 BUT if the actor is currently drinking then even if thirst is lower score it higher.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants