9

On PPCG, we frequently have King of the Hill challenges, which pit different code bots against each other. We don't like limiting these challenges to a single language, so we do cross-platform communication over standard I/O.

My goal is to write a framework that challenge-writers will be able use to make writing these challenges easier. I've come up with the following requirements I'd like to fulfill:

  1. The challenge-writer is able to make a class where methods represent each of the distinct communications. For example, on our Good vs Evil challenge, the writer would make a Player class that has an abstract boolean vote(List<List<Boolean>> history) method on it.

  2. The controller is able to provide instances of the above class that communicate via standard I/O when the aformentioned methods are called. That said, not all instances of the above class will necessarily communicate over standard I/O. 3 of the bots may be native Java bots (that simply override the Player class, where another 2 are in another language)

  3. The methods won't always have the same number of arguments (nor will they always have a return value)

  4. I'd like the challenge-writer to have to do as little work as possible to work with my framework.

I'm not against using reflection to solve these problems. I've considered requiring the challenge writer to do something like:

class PlayerComm extends Player {
    private Communicator communicator;
    public PlayerComm(Communicator communicator){
        this.communicator = communicator;
    }
    @Override
    boolean vote(List<List<Boolean>> history){
         return (Boolean)communicator.sendMessage(history);
    }
}

but if there are several methods, this can get pretty repetitive, and the constant casting isn't fun. (sendMessage in this example would accept a variable number of Object arguments, and return an Object)

Is there a better way to do this?

  • 1
    I'm confused about the "PlayerComm extends Player" -part. Are all the Java entrants extending Player, and this PlayerComm class is an adapter for a non-Java entrants? – ZeroOne Apr 5 '16 at 21:52
  • Yes, that is right – Nathan Merrill Apr 5 '16 at 22:05
  • So, out of curiosity... Did you manage to come up with some kind of nice solution for this? – ZeroOne Sep 6 '16 at 9:58
  • Nope. I don't think what I want is possible in Java :/ – Nathan Merrill Sep 6 '16 at 12:29
1

OK so things kind of escalated and I ended up with the following ten classes...

The bottom line in this method is that all communication happens using the Message class, i.e. the game never calls the players' methods directly but always using a communicator class from your framework. There's a reflection-based communicator for native Java classes and then there must be a custom communicator for all the non-Java players. Message<Integer> message = new Message<>("say", Integer.class, "Hello"); would initialize a message to a method named say with parameter "Hello" returning an Integer. This is then passed to a communicator (generated using a factory based on the player type) which then executes the command.

import java.util.Optional;

public class Game {
    Player player; // In reality you'd have a list here

    public Game() {
        System.out.println("Game starts");
        player = new PlayerOne();
    }

    public void play() {
        Message<Boolean> message1 = new Message<>("x", Boolean.class, true, false, true);
        Message<Integer> message2 = new Message<>("y", Integer.class, "Hello");
        Result result1 = sendMessage(player, message1);
        System.out.println("Response 1: " + result1.getResult());
        Result result2 = sendMessage(player, message2);
        System.out.println("Response 2: " + result2.getResult());
    }

    private Result sendMessage(Player player, Message<?> message1) {
        return Optional.ofNullable(player)
                .map(Game::createCommunicator)
                .map(comm -> comm.executeCommand(message1))
                .get();
    }

    public static void main(String[] args) {
        Game game = new Game();
        game.play();
    }

    private static PlayerCommunicator createCommunicator(Player player) {
        if (player instanceof NativePlayer) {
            return new NativePlayerCommunicator((NativePlayer) player);
        }
        return new ExternalPlayerCommunicator((ExternalPlayer) player);
    }
}

public abstract class Player {}

public class ExternalPlayer extends Player {}

public abstract class NativePlayer extends Player {
    abstract boolean x(Boolean a, Boolean b, Boolean c);
    abstract Integer y(String yParam);
    abstract Void z(Void zParam);
}

public abstract class PlayerCommunicator {
    public abstract Result executeCommand(Message message);
}

import java.lang.reflect.Method;
public class NativePlayerCommunicator extends PlayerCommunicator {
    private NativePlayer player;
    public NativePlayerCommunicator(NativePlayer player) { this.player = player; }
    public Result executeCommand(Message message) {
        try {
            Method method = player.getClass().getDeclaredMethod(message.getMethod(), message.getParamTypes());
            return new Result(method.invoke(player, message.getArguments()));
        } catch (Exception e) { throw new RuntimeException(e); }
    }
}

public class ExternalPlayerCommunicator extends PlayerCommunicator {
    private ExternalPlayer player;
    public ExternalPlayerCommunicator(ExternalPlayer player) { this.player = player; }
    @Override
    public Result executeCommand(Message message) { /* Do some IO stuff */ return null; }
}

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Message<OUT> {
    private final String method;
    private final Class<OUT> returnType;
    private final Object[] arguments;

    public Message(final String method, final Class<OUT> returnType, final Object... arguments) {
        this.method = method;
        this.returnType = returnType;
        this.arguments = arguments;
    }

    public String getMethod() { return method; }
    public Class<OUT> getReturnType() { return returnType; }
    public Object[] getArguments() { return arguments; }

    public Class[] getParamTypes() {
        List<Class> classes = Arrays.stream(arguments).map(Object::getClass).collect(Collectors.toList());
        Class[] classArray = Arrays.copyOf(classes.toArray(), classes.size(), Class[].class);
        return classArray;
    }
}

public class PlayerOne extends NativePlayer {
    @Override
    boolean x(Boolean a, Boolean b, Boolean c) {
        System.out.println(String.format("x called: %b %b %b", a, b, c));
        return a || b || c;
    }

    @Override
    Integer y(String yParam) {
        System.out.println("y called: " + yParam);
        return yParam.length();
    }

    @Override
    Void z(Void zParam) {
        System.out.println("z called");
        return null;
    }
}

public class Result {
    private final Object result;
    public Result(Object result) { this.result = result; }
    public Object getResult() { return result; }
}

(PS. Other keywords in my mind that I can't quite refine into anything useful right now: Command Pattern, Visitor Pattern, java.lang.reflect.ParameterizedType)

  • My goal is to prevent requiring the person who made Player from writing PlayerComm at all. While the communicator interfaces do automatic casting for me, I'd still run into the same problem of having to write the same sendRequest() function in each method. – Nathan Merrill Apr 5 '16 at 23:26
  • I have rewritten my answer. However, I now realize using the facade pattern might actually be the way to go here, by wrapping non-Java entries into what looks exactly like a Java entry. So no fooling around with some communicators or reflection. – ZeroOne Apr 6 '16 at 22:12
  • 2
    "OK so things kind of escalated and I ended up with the following ten classes" If I had a nickel... – Jack May 6 '16 at 23:55
  • Ow, my eyes! Anyway we could get a class diagram to go with these 10 classes? Or are you too busy writing your facade pattern answer? – candied_orange May 25 '16 at 23:09
  • @CandiedOrange, as a matter of fact I think I've already spent enough time with this question. I'm kind of hoping someone else would give their version of a solution, possibly using a facade pattern. – ZeroOne May 26 '16 at 20:22

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.