Ride the Hype

  • Pokemon is still popular
  • Fall this year
    • Pokemon Let's Go
      • Pikachu
      • Eevee
  • It's a planning problem
  • Party building
  • RPG
  • Strongly typed

https://pokemondb.net/pokedex/bulbasaur

I wanna be the very best...

  1. A full team
  2. Of different pokemon
  3. Strong against anything
  4. Weak against "nothing"...
    1. As few as possible

Optaplanner:

"Is this better?"

Consult Gloom

  • SQL-ish
  • Objects in Memory
  • Evaluate Rules

/domain/Pokemon.kt

class Pokemon{
    var id: Int? = null
    var name: String? = null
    var type1: Type? = null
    var type2: Type? = null
    var strongAgainst: List<String>
    var weakAgainst: List<String>

    constructor(id: Int?, name: String?, type1: Type?, type2: Type? = null) {
        ...
    }

    fun isStrongAgainst(type: Type): Boolean {
        return this.strongAgainst.contains(type.name.toString())
    }

    fun isWeakAgainst(type:Type): Boolean {
        return this.weakAgainst.contains(type.name.toString())
    }

    override fun toString(): String {
        return "Pokemon(name=$name)"
    }
}

/domain/Types.kt






data class Type(val name: TypeEnum,
                val strongAgainst: List<TypeEnum>,
                val weakAgainst: List<TypeEnum>,
                val blankAgainst: List<TypeEnum>)

val Normal = Type(
        name = TypeEnum.NORMAL,
        strongAgainst = listOf(),
        weakAgainst = listOf(TypeEnum.ROCK),
        blankAgainst = listOf(TypeEnum.GHOST)
)

val Fighting = Type(
        name = TypeEnum.FIGHTING,
        strongAgainst = listOf(TypeEnum.NORMAL, TypeEnum.ROCK, TypeEnum.ICE),
        weakAgainst = listOf(TypeEnum.POISON, TypeEnum.BUG, TypeEnum.PSYCHIC, TypeEnum.FLYING),
        blankAgainst = listOf(TypeEnum.GHOST)
)

...

val Types = listOf(
        Normal,
        Fighting,
        ...)

/domain/TeamSlot.kt

@PlanningEntity
class TeamSlot {

    var id: Int = 0

    @PlanningVariable(valueRangeProviderRefs = ["pokeRange"])
    var pokemon: Pokemon? = null

    override fun equals(other: Any?): Boolean {
        return this.id == (other as TeamSlot).id
    }

    override fun toString(): String {
        return "TeamSlot(id=$id, pokemon=${pokemon?.name})"
    }

    constructor()

    constructor(id: Int, pokemon: Pokemon?) {
        this.id = id
        this.pokemon = pokemon
    }
}

/domain/Team.kt

@PlanningSolution
class Team() {

    @PlanningEntityCollectionProperty
    lateinit var team: List<TeamSlot>

    @PlanningScore
    lateinit var score: HardSoftScore

    @ValueRangeProvider(id = "pokeRange")
    @ProblemFactCollectionProperty
    lateinit var pokemon: List<Pokemon>

    @ValueRangeProvider(id = "pokeTypes")
    @ProblemFactCollectionProperty
    lateinit var types: List<Type>

    constructor(team: List<TeamSlot>, pokemon: List<Pokemon>, types: List<Type>): this() {
        this.team = team
        this.pokemon = pokemon
        this.types = types
    }

    override fun toString(): String {
        return "Team(team=$team, score=$score)"
    }
}

solverConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<solver>
    <!--<environmentMode>FULL_ASSERT</environmentMode>-->
    <!--<environmentMode>FAST_ASSERT</environmentMode>-->
    <solutionClass>net.spantree.kantoptimal.domain.Team</solutionClass>
    <entityClass>net.spantree.kantoptimal.domain.TeamSlot</entityClass>

    <scoreDirectorFactory>
        <scoreDrl>rules.drl</scoreDrl>
    </scoreDirectorFactory>

    <termination>
        <secondsSpentLimit>10</secondsSpentLimit>
        <bestScoreLimit>0hard/0soft</bestScoreLimit>
    </termination>
    <constructionHeuristic/>
    <localSearch>
        <acceptor>
            <simulatedAnnealingStartingTemperature>2hard/100soft</simulatedAnnealingStartingTemperature>
        </acceptor>
        <forager>
            <acceptedCountLimit>4</acceptedCountLimit>
        </forager>
    </localSearch>
</solver>

KantoptimalApp.kt

@SpringBootApplication
class KantoptimalApp {

    @Bean
    fun init() = CommandLineRunner {
        println("ok")
    }

    fun main(args: Array<String>) {
        runApplication<KantoptimalApp>(*args)
        val solution = Team(listOf(
                TeamSlot(1, null),
                TeamSlot(2, null),
                TeamSlot(3, null),
                TeamSlot(4, null),
                TeamSlot(5, null),
                TeamSlot(6, null)
        ),
                POKEDEX.values.toList(),
                Types
        )

        val solverFactory: SolverFactory<Team> = SolverFactory.createFromXmlResource(
                "solverConfig.xml")
        val solver: Solver<Team> = solverFactory.buildSolver()
        val newTeam = solver.solve(solution)
        val scoreDirector = solver.scoreDirectorFactory.buildScoreDirector()
        println(newTeam)
        scoreDirector.workingSolution = newTeam
    }
}

No Rules!

Oh...

rule "no duplicates"
    when
        TeamSlot($pokemon1: pokemon, pokemon != null)
        TeamSlot(pokemon == $pokemon1)
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end

Oh...



rule "STAB (same-type-attack-bonus) against everything"
  when
        $type: Type()
        not (exists TeamSlot(pokemon != null, pokemon.isStrongAgainst($type) == true))
  then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end

Viability!

  • Geodude
  • Paras
  • Jynx
  • Mankey
  • Gastly
  • Bulbasaur
  • Weak against: [ROCK, ICE, BUG, GRASS, FIGHTING, POISON, PSYCHIC, GHOST, GROUND]


rule "fewest weaknesses"
    when
        $type: Type()
        TeamSlot(pokemon.isWeakAgainst($type), pokemon != null)

    then
        scoreHolder.addSoftConstraintMatch(kcontext, -1);
end

Better!

  • Jynx
  • Hitmonlee
  • Electrode
  • Pinsir
  • Diglett
  • Haunter
  • Weak against: 8 types
    • 9 before

// TODO

  • Remove STAB assumption
  • Special/Physical
  • Remodeling

Sources

  • https://pokemondb.net/
  • https://assets.pokemon.com/

Thanks

  • Shane
  • Kevin
  • Cedric

Optaplanner Let's Go

By dlindema

Optaplanner Let's Go

  • 441