anchor.diff:  %             {false,false} -> {Min,Max}
   %         end
   %     end,
  -%     lists:foldl(MinMax, {[nil|<<255>>],-1.0e100}, L). +%     lists:foldl(MinMax, {[nil|<<255>>],-1.0e100}, L). 
  +
  +%% ===================================================================
  +%% TESTS
  +%% ===================================================================
  +
  +-ifdef(TEST).
  +
  +-include_lib("eunit/include/eunit.hrl").
  +
  +qlearn_max_test_() ->
  +    W = 3,
  +    H = 3,
  +    Board = lists:seq($a,$a+W*H-1),
  +    [ {"{1,1}",   ?_assertEqual({1,1}, qlearn_max([0,0,{0,0},{1,0},{1,1}]))}
  +    , {"{1,0}", ?_assertEqual({1,1}, qlearn_max([0,0,{0,0},{1,0},{1,-1}]))}
  +    , {"{1,-0.5}", ?_assertEqual({1,1}, qlearn_max([0,0,{0,0},{1,-0.5},{1,-1}]))}
  +    ].
  +
  +-endif.
  
positive.code: handle_call(start_learning, _From, State) ->
      {reply, ok, State#state{status=learning}};
  handle_call({predict, Input}, _From, #state{network=Network, status=playing} = State) ->
      Out = ann:predict(Network, Input),
      {reply, Out, State};
  handle_call({predict, _}, _From, State) ->
      {reply, ?ANN_NOT_PLAYING, State};
  
negative.code: match_val(V) -> V.
  
  
  %% Sample all moves of a game and aggregate Q-learned winning chances into the training table
  %% Key in the training table is a hash of the (alias normalized and symmetry normalized) board atate before the move (action) 
  %% This follows an adapted the Q-learnng idea where the winning estimate of the current {position, action}
  %% is calculated as the negated (maximum) winning chance of the opponent in that position  
  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_samples_game(GameTypeId, egambo_tictac:samples(Space, Ialiases, Moves, Naliases, Nscores), Params, Alfa, Gamma, undefined,[]).
  
  %% Scan the moves of a game from last to first and update an aggregated Q-learning table which can also be used to train an ANN (for interpolation)
  ann_samples_game(_, [], _, _, _, _, Acc) -> lists:reverse(Acc);
  ann_samples_game(GameTypeId, [Sample|Samples], Params, Alfa, Gamma, LastTrainingRec, Acc) ->
      {Hash, Output, TrainingRec} = ann_sample_single(GameTypeId, Sample, Params, Alfa, Gamma, LastTrainingRec),
      ann_samples_game(GameTypeId, Samples, Params, Alfa, Gamma, TrainingRec, [{Hash, Output}|Acc]).
  
  %% 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
  %% Score is the final score of the game as seen by the current player
  %% LastTrainingRec is the updated training data for the next move (played after the current one)
  %% LastTrainingRec is containing all Q(Board,Moves) as seen in the perspective of the opponent
  %% The Q-learning back-propagation needs to negate this Q for the current player
  %% The estimate for the quality of this move hence is the negated current Q of the LastTrainingRec  
  ann_sample_single(GameTypeId, [Board, _Players, Move, [Score|_], MTE], Params, Alfa, Gamma, LastTrainingRec) ->
      #{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]),
      Q = case MTE of
          0 -> Score;
          _ -> 0
      end,
      {Output, 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, Q};      % 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, OL};
          true ->
              OL = Width,
              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, Q};      % 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, OL}
      end,
      % ?Info("ann_sample_single Output ~p OutputIdx ~p", [Output, OMove]),
      Hash = erlang:phash2(Input, ?ANN_HASH_BASE),
      case ann_sample_training(Input, Output, MTE, OMove, Olen, Alfa, Gamma, {GameTypeId, Hash}, LastTrainingRec) of
          hash_collision ->       % Hash is already used for another board
              case ann_sample_training(Input, Output, MTE, OMove, Olen, Alfa, Gamma, {GameTypeId, -Hash}, LastTrainingRec) of    
                  hash_collision ->   % Hash is already used for another board
                      ?Info("Unrecoverable input hash collision for Hash=~p Input=~p, sample ignored", [Hash, Input]),
                      {0, Output, undefined};
                  AltTrainingRec ->
                      imem_meta:write(?ANN_TRAIN, AltTrainingRec),
                      {-Hash, Output, AltTrainingRec}
              end;
          TrainingRec ->
              imem_meta:write(?ANN_TRAIN, TrainingRec),
              {Hash, Output, TrainingRec}
      end.
  
  ann_sample_training(Input, Output, 0, _OMove, _Olen, _Alfa, _Gamma, Key, undefined) ->
      case imem_meta:read(?ANN_TRAIN, Key) of
          [] ->
              % TrainingRec does not exist yet, create it   
              #egTicTacAnnSample{skey=Key, qos=1, input=Input, output=Output};
          [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] ->
              % Immediate win or tie, cannot expect next Q there. Record the observed Score by adding to the accu
              #egTicTacAnnSample{skey=Key, qos=N+1, input=Input, output=vector_add(OldOut, Output)};
          _ ->
              hash_collision
      end;
  ann_sample_training(Input, Output, _MTE, _OMove, _Olen, _Alfa, _Gamma, Key, undefined)  ->
      case imem_meta:read(?ANN_TRAIN, Key) of
          [] ->
              % TrainingRec does not exist yet, create it   
              #egTicTacAnnSample{skey=Key, qos=1, input=Input, output=Output};
          [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] ->
              % Not the last move but cannot Q-learn from next action (record just processed is lost due to hash collision)
              #egTicTacAnnSample{skey=Key, qos=N+1, input=Input, output=vector_add(OldOut, Output)}
      end;
  ann_sample_training(Input, Output, MTE, OMove, Olen, Alfa, Gamma, Key, LastTrainingRec) when MTE>0 ->
      case imem_meta:read(?ANN_TRAIN, Key) of
          [] ->
              % TrainingRec does not exist yet, create it   
              #egTicTacAnnSample{output=NextOut} = LastTrainingRec,
              % ?Info("LastTrainingRec ~p",[LastTrainingRec]),
              #egTicTacAnnSample{skey=Key, qos=1, input=Input, output=qlearn_add(Output, Output, NextOut, OMove, Olen, Alfa, Gamma)};
          [#egTicTacAnnSample{qos=N, input=Input, output=OldOut}] ->
              % Not the last move, Q-learn from next action (just processed)
              #egTicTacAnnSample{output=NextOut} = LastTrainingRec,
              % ?Info("LastTrainingRec ~p",[LastTrainingRec]),
              #egTicTacAnnSample{skey=Key, qos=N+1, input=Input, output=qlearn_add(OldOut, Output, NextOut, OMove, Olen, Alfa, Gamma)};
          _ ->
              hash_collision
      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)].
  