@Override
  protected State actionHelper(State s, JointAction ja) {

    List<GroundedSGAgentAction> gsas = ja.getActionList();

    // need to force no movement when trying to enter space of a noop agent
    List<Location2> previousLocations = new ArrayList<GridGameStandardMechanics.Location2>();
    List<Location2> noopLocations = new ArrayList<GridGameStandardMechanics.Location2>();

    for (GroundedSGAgentAction gsa : gsas) {
      Location2 loc = this.getLocation(s, gsa.actingAgent);
      previousLocations.add(loc);
      if (gsa.action.actionName.equals(GridGame.ACTIONNOOP)) {
        noopLocations.add(loc);
      }
    }

    List<Location2> basicMoveResults = new ArrayList<GridGameStandardMechanics.Location2>();
    for (int i = 0; i < ja.size(); i++) {
      Location2 loc = previousLocations.get(i);
      GroundedSGAgentAction gsa = gsas.get(i);
      basicMoveResults.add(
          this.sampleBasicMovement(
              s, loc, this.attemptedDelta(gsa.action.actionName), noopLocations));
    }

    // resolve swaps
    basicMoveResults = this.resolvePositionSwaps(previousLocations, basicMoveResults);

    List<Location2> finalPositions = this.resolveCollisions(previousLocations, basicMoveResults);
    for (int i = 0; i < finalPositions.size(); i++) {
      GroundedSGAgentAction gsa = gsas.get(i);
      Location2 loc = finalPositions.get(i);

      ObjectInstance agent = s.getObject(gsa.actingAgent);
      agent.setValue(GridGame.ATTX, loc.x);
      agent.setValue(GridGame.ATTY, loc.y);
    }

    return s;
  }
  @Override
  public List<TransitionProbability> transitionProbsFor(State s, JointAction ja) {

    List<TransitionProbability> tps = new ArrayList<TransitionProbability>();

    List<GroundedSGAgentAction> gsas = ja.getActionList();

    // need to force no movement when trying to enter space of a noop agent
    List<Location2> previousLocations = new ArrayList<GridGameStandardMechanics.Location2>();
    List<Location2> noopLocations = new ArrayList<GridGameStandardMechanics.Location2>();

    for (GroundedSGAgentAction gsa : gsas) {
      Location2 loc = this.getLocation(s, gsa.actingAgent);
      previousLocations.add(loc);
      if (gsa.action.actionName.equals(GridGame.ACTIONNOOP)) {
        noopLocations.add(loc);
      }
    }

    List<List<Location2Prob>> possibleOutcomes = new ArrayList<List<Location2Prob>>();
    for (int i = 0; i < ja.size(); i++) {
      Location2 loc = previousLocations.get(i);
      GroundedSGAgentAction gsa = gsas.get(i);
      possibleOutcomes.add(
          this.getPossibleLocationsFromWallCollisions(
              s, loc, this.attemptedDelta(gsa.action.actionName), noopLocations));
    }

    List<LocationSetProb> outcomeSets = this.getAllLocationSets(possibleOutcomes);

    for (LocationSetProb sp : outcomeSets) {

      // resolve collisions from attempted swaps, which is deterministic and does not need to be
      // recursed
      List<Location2> basicMoveResults = this.resolvePositionSwaps(previousLocations, sp.locs);

      // finally, we need to find all stochastic outcomes from cell competition
      List<LocationSetProb> cOutcomeSets =
          this.getPossibleCollisionOutcomes(previousLocations, basicMoveResults);

      // turn them into states with probabilities
      for (LocationSetProb csp : cOutcomeSets) {

        State ns = s.copy();
        for (int i = 0; i < csp.locs.size(); i++) {
          GroundedSGAgentAction gsa = gsas.get(i);
          Location2 loc = csp.locs.get(i);

          ObjectInstance agent = ns.getObject(gsa.actingAgent);
          agent.setValue(GridGame.ATTX, loc.x);
          agent.setValue(GridGame.ATTY, loc.y);
        }

        double totalProb = sp.p * csp.p;
        TransitionProbability tp = new TransitionProbability(ns, totalProb);
        tps.add(tp);
      }
    }

    return this.combineDuplicateTransitionProbabilities(tps);
  }