anchor.diff:          , save/3
           , ann_sample/7
           , ann_samples/7
  +        , ann_sample/9
  +        , ann_samples/9
           , read_samples/6
           , predict/2
           , pick_output/3
  
positive.code: -define(ANN_ACTIVATION, tanh1).     % tanh1, tanh, relu or sigmoid
  -define(ANN_OUTPUT_TARGET, 1.0).    % Output swing of 'most important move' (should correlate to point of max. nonlinearity of AF)
  -define(ANN_OUTPUT_MTE, 4.0).       % Optimize for sensitivity for move 4 before the end of the game (4 more half-moves to end)
  -define(ANN_OUTPUT_UNSAMPLED, 0.001).       % Value emitted as output for unsampled move in Export 
  
  
  -define(ANN_NO_TEST_SAMPLES, {error, <<"No test samples found">>}).
  
negative.code: %% Sample one random move out of a game into the training table
  %% and aggregate into the training table using an input hash
  ann_sample(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores) ->
      ann_sample(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores, ?ANN_QLEARN_ALFA, ?ANN_QLEARN_GAMMA).
  
  ann_sample(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores, Alfa, Gamma) ->
      #egGameType{params=Params} = egambo_game:read_type(GameTypeId),
      [Input, _Players, Move, [Score|_], MTE] = egambo_tictac:sample(Space, Ialiases, Moves, Naliases, Nscores),
      ann_sample_single(GameTypeId, Input, Move, Score, MTE, Ialiases, Params, Alfa, Gamma).
  
  %% Sample all moves of a game into the training table (ok to do as long as we have enough ramdomness)
  %% and aggregate into the training table using an input hash
  ann_samples(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores) ->
      ann_samples(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores, ?ANN_QLEARN_ALFA, ?ANN_QLEARN_GAMMA).
  
  ann_samples(GameTypeId, _GameId, Space, Ialiases, Moves, Naliases, Nscores, Alfa, Gamma) ->
      #egGameType{params=Params} = egambo_game:read_type(GameTypeId),
      [ ann_sample_single(GameTypeId, Board, Move, Score, MTE, Ialiases, Params, Alfa, Gamma) || 
          [Board, _Players, Move, [Score|_], MTE] <- egambo_tictac:samples(Space, Ialiases, Moves, Naliases, Nscores)
      ].
  
  %% Format a single (already Alias-normalized) sample for ann use (Board size input vector, Board size / Board Width output vector)
  %% and aggregate into the training table using an input hash
  ann_sample_single(GameTypeId, Board, Move, Score, MTE, Ialiases, Params, Alfa, Gamma) ->
      #{width:=Width, height:=Height, gravity:=Gravity, periodic:=Periodic} = Params,
      % ?Info("ann_sample_single Board and Move ~p ~p", [Board, Move]),
      {Input, Sym} = egambo_tictac_sym:norm(Width, Height, Gravity, Periodic, Board),
      % ?Info("ann_sample_single Input and Sym ~p ~p", [Input, Sym]),
      Gain = ann_sample_gain(Width, Height, Score, MTE),
      {Output, IMove, OMove, Olen} = case Gravity of
          false ->
              OL = Width*Height,
              OMove1 = egambo_tictac_sym:map_move(Width, Height, Move, Sym) + 1,              % one based index of move
              F = fun(I) -> 
                  Inp=lists:nth(I, Input), 
                  if 
                      I==OMove1 -> {1, Gain};   % move taken in sample
                      Inp==32 ->   {0, 0};      % alternative legal move, not taken here
                      true ->      0            % illegal move (occupied)
                  end 
              end, 
              {lists:map(F, lists:seq(1, OL)), OMove1, OMove1, OL};
          true ->
              OL = Width,
              IMove1 = egambo_tictac_sym:map_move(Width, Height, Move, Sym) + 1,              % one based index to Input
              OMove1 = egambo_tictac_sym:map_move(Width, Height, Move rem Width, Sym) + 1,    % one based index to Output
              F = fun(I) -> 
                  Inp=lists:nth(I, Input), 
                  if 
                      I==OMove1 -> {1, Gain};   % move taken in sample
                      Inp==32 ->   {0, 0};      % alternative legal move, not taken here
                      true ->      0            % illegal move (occupied)
                  end 
              end, 
              {lists:map(F, lists:seq(1, OL)), IMove1, OMove1, OL}
      end,
      % ?Info("ann_sample_single Output ~p InputIdx ~p OutputIdx ~p", [Output, IMove, OMove]),
      Hash = erlang:phash2(Input, ?ANN_HASH_BASE),
      Key = {GameTypeId, Hash},
      case imem_meta:read(?ANN_TRAIN, Key) of
          [] ->   
              imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=Key
                                              , qos=1, input=Input, output=Output}),
              {Hash, Output};
          [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] when MTE==0 ->
              % Immediate win or tie, cannot expect a Q there
              imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=Key
                                              , qos=N+1, input=Input
                                              , output=vector_add(OldOut, Output)}),
              {Hash, Output};
          [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] ->
              ChildInput = apply_move1(Input, IMove, hd(Ialiases)),
              ChildHash = erlang:phash2(ChildInput, ?ANN_HASH_BASE),
              ChildKey = {GameTypeId, ChildHash},
              case imem_meta:read(?ANN_TRAIN, ChildKey) of
                  [#egTicTacAnnSample{input=ChildInput, output=ChildOut}] ->
                      imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=Key
                                                      , qos=N+1, input=Input
                                                      , output=qlearn_add(OldOut, Output, ChildOut, OMove, Olen, Alfa, Gamma)}),
                      {Hash, Output};
                  _ ->
                      imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=Key
                                                      , qos=N+1, input=Input
                                                      , output=vector_add(OldOut, Output)}),
                      {Hash, Output}
              end;
          _ ->
              % hash collision detected, try to escape to negative hash
              AltKey = {GameTypeId, -Hash},
              case imem_meta:read(?ANN_TRAIN, AltKey) of
                  [] ->   
                      imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=AltKey
                                                      , qos=1, input=Input, output=Output}),
                      {-Hash, Output};
                  [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] when MTE==0 ->
                      imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=AltKey
                                                  , qos=N+1, input=Input
                                                  , output=vector_add(OldOut, Output)}),
                      {-Hash, Output};
                  [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] ->
                      ChildInput = apply_move1(Input, IMove, hd(Ialiases)),
                      ChildHash = erlang:phash2(ChildInput, ?ANN_HASH_BASE),
                      ChildKey = {GameTypeId, ChildHash},
                      case imem_meta:read(?ANN_TRAIN, ChildKey) of
                          [#egTicTacAnnSample{input=ChildInput, output=ChildOut}] ->
                              imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=AltKey
                                                              , qos=N+1, input=Input
                                                              , output=qlearn_add(OldOut, Output, ChildOut, OMove, Olen, Alfa, Gamma)}),
                              {-Hash, Output};
                          _ ->
                              imem_meta:write(?ANN_TRAIN, #egTicTacAnnSample{skey=AltKey
                                                              , qos=N+1, input=Input
                                                              , output=vector_add(OldOut, Output)}),
                              {-Hash, Output}
                      end;
                  [#egTicTacAnnSample{input=CollInput}] ->
                      ?Info("Unrecoverable input hash collision OldInput= ~p  NewInput=~p ignored",[CollInput, Input]),
                      {0, Output}
              end
      end.
  
  apply_move1(Input, 1, Alias) ->
      [Alias|tl(Input)];
  apply_move1(Input, Move1, Alias) -> 
      {H,T} = lists:split(Move1-1, Input),
      H ++ [Alias|tl(T)]. 
  
  
  %% Output (gain) of a single sample output item depending on the MTE (moves to end)
  %% An MTE of 0 (a win) is placed somewhat above the higest sensitivity of the AF
  %% An MTE of 1 (a loose/tie) is placed somewhat below higest sensitivity in the negative
  %% A (roughly) symmetric AF around the origin is assumed here
  ann_sample_gain(Width, Height, Score, MTE) when Score >= 0.0 ->
      ?ANN_OUTPUT_TARGET * Score * (Width*Height-MTE) / (Width*Height-?ANN_OUTPUT_MTE);
  ann_sample_gain(Width, Height, Score, MTE) ->
      ?ANN_OUTPUT_TARGET * Score * (Width*Height-MTE+1) / (Width*Height-?ANN_OUTPUT_MTE).
  
  vector_add(OldOut, Output) ->
      [vector_add_one(Old, Out) || {Old, Out} <- lists:zip(OldOut, Output)].
  
  vector_add_one(0, 0) -> 0;
  vector_add_one({AccN,AccQ}, {N,Q}) -> {AccN+N, AccQ+Q}. 
  
  qlearn_add(OldOut, Output, ChildOutput, OMove, Olen, Alfa, Gamma) ->
      [qlearn_add_one(Old, Out, Pos, ChildOutput, OMove, Alfa, Gamma) || {Old, Out, Pos} <- lists:zip3(OldOut, Output, lists:seq(1, Olen))].
  
  qlearn_add_one({AccN,AccQ}, {N,Q}, OMove, ChildOutput, OMove, Alfa, Gamma) ->
      {CN,CQ} = qlearn_max(ChildOutput),
      AccNN = AccN+N,
      AccQN = AccNN*AccQ/AccNN,
      {AccNN, AccQN + Alfa*(Q*AccNN/N + Gamma*CQ*AccNN/CN - AccQN)};
  qlearn_add_one(Old, Out, _, _, _, _, _) -> vector_add_one(Old, Out).
  
  qlearn_max(ChildOutput) -> qlearn_max(ChildOutput,{0,0},0).
  
  qlearn_max([], Max, _) -> Max;  
  qlearn_max([{0,_}|ChildOutput], Max, MaxQ) -> qlearn_max(ChildOutput, Max, MaxQ);  
  qlearn_max([{N,Q}|ChildOutput], _Max, MaxQ) when Q/N>MaxQ -> qlearn_max(ChildOutput, {N,Q}, Q/N);
  qlearn_max([{N,Q}|ChildOutput], Max, MaxQ) when Q/N>MaxQ -> qlearn_max(ChildOutput, Max, MaxQ).
  
  ann_norm_input(L) -> [ann_norm_single_input(I) || I <- L].
  
  