3
\$\begingroup\$

I posted this on SO first, but was redirected here, since the code below already works:


I'm currently trying to develop a small game as a hobby project, which contains an automated combat system based on cards. My goal is to make later implementation easier by providing a fluent API, like this:

doEffect().to(<target>) // applying 1 effect to 1 target
doEffect().to(<targets>) // applying 1 effect to multiple targets

doMultiple(doEffect1(), doEffect2()).to(...) // applying multiple effects
doEffect1().and(doEffect2()).to(...) // different syntax

Apart from providing targets upfront, it should also be possible to have them resolved at runtime, like "target 1 enemy with the lowest life".

When applying multiple effects to the same targets (by either joining them with doMultiple or and), the targets need to be resolved before any effects, since conditions might change after the first effect and change targets, otherwise. Also, certain effects have beforeExecute and afterExecute conditions, which should only be executed once before/after all targets have been processed.

The version below is fully functional, but I would like to know, how my design could be optimized, since I haven't been using streams and lambdas for too long:

@FunctionalInterface
public interface Effect {
    public void execute(CharacterRelativeContext context);
}

 

public abstract class TargetedEffect {
    /**
     * (optional) this is called exactly once, before the effect is applied to
     * any targets
     */
    protected void beforeExecute(CharacterRelativeContext context) {

    }

    /**
     * (optional) this is called exactly once, after the effect has been applied
     * to all targets
     */
    protected void afterExecute(CharacterRelativeContext context) {

    }

    /**
     * applies this effect to a single target. For setup or cleanup
     * functionality that should only be executed once for any amount of
     * targets, use {@link #beforeExecute} or {@link #afterExecute}.
     */
    protected abstract void execute(CharacterRelativeContext context, CharacterState target);

    public Effect to(CharacterState... targets) {
        return targets.length == 1 ? this.to(targets[0]) : this.to(Arrays.asList(targets));
    }

    public Effect to(CharacterState target) {
        return c -> {
            this.beforeExecute(c);
            this.execute(c, target);
            this.afterExecute(c);
        };
    }

    public Effect to(Collection<CharacterState> targets) {
        return c -> {
            this.beforeExecute(c);
            targets.forEach(t -> this.execute(c, t));
            this.afterExecute(c);
        };
    }

    public Effect to(TargetSingle target) {
        return c -> this.to(target.resolve(c)).execute(c);
    }

    public Effect to(TargetMultiple target) {
        return c -> this.to(target.resolve(c)).execute(c);
    }

    public TargetedEffect and(TargetedEffect other) {
        return new Joined(this, other);
    }

    public static TargetedEffect doMultiple(TargetedEffect... effects) {
        return new Joined(effects);
    }

    private static final class Joined extends TargetedEffect {
        private final List<TargetedEffect> effects = new ArrayList<>();

        public Joined(TargetedEffect... effects) {
            Collections.addAll(this.effects, effects);
        }

        @Override
        protected void execute(CharacterRelativeContext context, CharacterState target) {

        }

        @Override
        public Effect to(CharacterState target) {
            return c -> this.effects.forEach(e -> e.to(target).execute(c));
        }

        @Override
        public Effect to(Collection<CharacterState> targets) {
            return c -> this.effects.forEach(e -> e.to(targets).execute(c));
        }

        /**
         * Overriden, so we don't create unnecessary wrapper objects
         */
        @Override
        public TargetedEffect and(TargetedEffect other) {
            this.effects.add(other);
            return this;
        }
    }
}

 

@FunctionalInterface
public interface TargetSingle {
    public CharacterState resolve(CharacterRelativeContext context);
}

 

@FunctionalInterface
public interface TargetMultiple {
    public Collection<CharacterState> resolve(CharacterRelativeContext context);
}

About the classes that are not shown:

  • CharacterState contains all mutable variables about a combat actor
  • CharacterRelativeContext contains convenience methods to access the current character, its allies and enemies. The effect itself might now even need access to it, but Target<...> classes use it to resolve targets.

As of now, I might want to add some logic to TargetedEffect.Joined() and TargetedEffect.Joined.and() to check whether it gets passed other Joined-instances and break them down into their subeffects first.


edit: Here's the implementation for CharacterRelativeContext:

public class CharacterRelativeContext {
    private final List<CharacterState> allies;
    private final List<CharacterState> enemies;
    private final CharacterState self;

    public CharacterRelativeContext(String selfId) {
        // this is an inner, non-static class of my current battlestate, which gets used to initialize the fields
    }

    public CharacterState getSelf() {
        return this.self;
    }

    public List<CharacterState> getAllies() {
        return this.allies;
    }

    public List<CharacterState> getEnemies() {
        return this.enemies;
    }
}

And a simplified use case of a targeted effect:

public class Armor extends TargetedEffect {
    private final int value;

    public Armor(int value) {
        this.value = value;    
    }

    @Override
    protected void execute(CharacterRelativeContext context, CharacterState target) {
        target.addArmor(this.value);
    }
}

There's no direct implementation of Effect, yet, though ultimately every TargetedEffect gets converted into one via one of its to methods.

One use case for actual effects would be a conditional that's checked on either the current actor or the battle environment (though currently all effects are directly attached to a character and not global).

\$\endgroup\$
  • \$\begingroup\$ Hi ! Can you show us the CharacterRelativeContext class as well as a use-case of your Effect ? Looking at your code, it feels like Effect don't bring much to the table as the more complicated logic will be put inside TargetedEffect, so I'd like to take a look at an existent use case. \$\endgroup\$ – Ronan Dhellemmes Mar 16 '18 at 10:32
  • \$\begingroup\$ I updated the question. And yes, right now there's no direct implementation of Effect, as it mostly gets used to convert targeted effects into untargeted ones, since targets get resolved during execution. \$\endgroup\$ – Iavra Mar 16 '18 at 10:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.