struct NullInfoState <: CFR.AbstractInfoState end

# remove game from AbstractCFRSolver type parameters?
struct TabularStrategy{K,G} <: CFR.AbstractCFRSolver{K,G,NullInfoState}
    game::G
    d::Dict{K,Vector{Float64}}
end

# function TabularStrategy(game::Game{H,K}, dict::Dict{K,<:AbstractVector}) where {H,K}
#     return TabularStrategy{K, typeof(game)}(dict)
# end

CFR.strategy(sol::TabularStrategy{K}, I::K) where K = sol.d[I]

const α_opt = 0.10

const OptimalKuhnStrategy = TabularStrategy(CFR.Kuhn(), Dict(
    (1,1,SA[-1,-1,-1]) => [1. - α_opt, α_opt],
    (1,2,SA[-1,-1,-1]) => [1., 0.],
    (1,3,SA[-1,-1,-1]) => [1-3α_opt,3α_opt],

    (2,1,SA[0,-1,-1]) => [2/3,1/3],
    (2,2,SA[0,-1,-1]) => [1., 0.],
    (2,3,SA[0,-1,-1]) => [0., 1.],

    (2,1,SA[1,-1,-1]) => [1., 0.],
    (2,2,SA[1,-1,-1]) => [2/3,1/3],
    (2,3,SA[1,-1,-1]) => [0., 1.],

    (1,1,SA[0,1,-1]) => [1., 0.],
    (1,2,SA[0,1,-1]) => [2/3-α_opt,1/3+α_opt],
    (1,3,SA[0,1,-1]) => [0., 1.]
))

const SuboptimalKuhnStrategy = TabularStrategy(CFR.Kuhn(), Dict(
    (1,1,SA[-1,-1,-1]) => normalize!(rand(2),1),
    (1,2,SA[-1,-1,-1]) => normalize!(rand(2),1),
    (1,3,SA[-1,-1,-1]) => normalize!(rand(2),1),

    (2,1,SA[0,-1,-1]) => normalize!(rand(2),1),
    (2,2,SA[0,-1,-1]) => normalize!(rand(2),1),
    (2,3,SA[0,-1,-1]) => normalize!(rand(2),1),

    (2,1,SA[1,-1,-1]) => normalize!(rand(2),1),
    (2,2,SA[1,-1,-1]) => normalize!(rand(2),1),
    (2,3,SA[1,-1,-1]) => normalize!(rand(2),1),

    (1,1,SA[0,1,-1]) => normalize!(rand(2),1),
    (1,2,SA[0,1,-1]) => normalize!(rand(2),1),
    (1,3,SA[0,1,-1]) => normalize!(rand(2),1),
))

const OptimalRPSStrategy = TabularStrategy(CFR.IIEMatrixGame(), Dict(
    0 => fill(1/3,3),
    1 => fill(1/3,3)
))

const SuboptimalRPSStrategy = TabularStrategy(CFR.IIEMatrixGame(), Dict(
    0 => Float64[1,0,0],
    1 => Float64[1,0,0]
))

const SlightlySuboptimalRPSStrategy = TabularStrategy(CFR.IIEMatrixGame(), Dict(
    0 => Float64[0.35,0.30,0.35],
    1 => Float64[0.30,0.35,0.35]
))

@testset "Exploitability" begin
    @testset "RPS" begin
        atol = 1e-3

        RPS = CFR.IIEMatrixGame()
        e_sol1 = ExploitabilitySolver(RPS, 1)
        e_opt1 = exploitability(e_sol1, OptimalRPSStrategy)

        e_sol2 = ExploitabilitySolver(RPS, 2)
        e_opt2 = exploitability(e_sol2, OptimalRPSStrategy)

        @test ≈(e_opt1, 0.0, atol=atol)
        @test ≈(e_opt2, 0.0, atol=atol)

        e_sol1 = ExploitabilitySolver(RPS, 1)
        e_ssopt1 = exploitability(e_sol1, SlightlySuboptimalRPSStrategy)

        e_sol2 = ExploitabilitySolver(RPS, 2)
        e_ssopt2 = exploitability(e_sol2, SlightlySuboptimalRPSStrategy)

        @test e_ssopt1 > 0.0
        @test e_ssopt2 > 0.0

        e_sol1 = ExploitabilitySolver(RPS, 1)
        e_sopt1 = exploitability(e_sol1, SuboptimalRPSStrategy)

        e_sol2 = ExploitabilitySolver(RPS, 2)
        e_sopt2 = exploitability(e_sol2, SuboptimalRPSStrategy)

        @test e_sopt1 > 0.0
        @test e_sopt2 > 0.0

        @test e_sopt1 > e_ssopt1 > e_opt1
        @test e_sopt2 > e_ssopt2 > e_opt2
    end

    @testset "Kuhn" begin
        atol = 1e-3

        kuhn = CFR.Kuhn()
        e_sol1 = ExploitabilitySolver(kuhn, 1)
        e_opt1 = exploitability(e_sol1, OptimalKuhnStrategy)

        e_sol2 = ExploitabilitySolver(kuhn, 2)
        e_opt2 = exploitability(e_sol2, OptimalKuhnStrategy)

        @test ≈(e_opt1, 0.0, atol=atol)
        @test ≈(e_opt2, 0.0, atol=atol)

        e_sol1 = ExploitabilitySolver(kuhn, 1)
        e_sopt1 = exploitability(e_sol1, SuboptimalKuhnStrategy)

        e_sol2 = ExploitabilitySolver(kuhn, 2)
        e_sopt2 = exploitability(e_sol2, SuboptimalKuhnStrategy)

        @test e_sopt1 > 0.0
        @test e_sopt2 > 0.0
    end
end
