I've written a half-decent AI in the formula AI branch. I'm posting the code here with commentary to demonstrate how (relatively) simple it makes it to make an AI.

This AI is designed to run on Hamlets, as player 2, playing the Undead. It could be generalized without too much trouble. It has an opening sequence that Soliton wrote.

Firstly we have the opening...it's just a function which will return a bunch of hard-wired moves based on the current turn. Programming an opening like this allows us to optimize the AI's opening moves.

Code:

        [ai]
         [function]
         name=opening
         inputs="ai"
         formula="
         if(ai.turn = 1,
                     #turn 1
                     [ recruit('Skeleton Archer', loc(11,21)),
                       recruit('Dark Adept', loc(11,22)),
                       recruit('Dark Adept', loc(10,22)),
                       recruit('Skeleton Archer', loc(9,22)),
                       recruit('Ghost', loc(11,24)),
                       move(loc(11,23), loc(14,22)) ],
                  if(ai.turn = 2,
                     #turn 2
                     [ move(loc(11,21),loc(13,17)),
                       if(unit_at(loc(11,22)).total_movement = 6,
                           move(loc(11,22),loc(13,18)),
                           move(loc(11,22),loc(15,19))),
                       move(loc(10,22),loc(7,19)),
                       move(loc(9,22),loc(4,22)),
                       move(loc(11,24),loc(18,24)),
                       move(loc(14,22),loc(11,23)),
                       recruit('Dark Adept', loc(11,21)),
                       recruit('Dark Adept', loc(11,22)) ],
                  if(ai.turn = 3,
                     #turn 3
                     [ move(loc(18,24),loc(20,22)),
                       move(loc(15,19),loc(17,17)),
                       move(loc(4,22),loc(5,18)),
                       recruit('Skeleton Archer') ],
                  if(ai.turn = 4,
                     #turn 4
                     [ move(loc(20,22),loc(20,15)),
                       recruit('Skeleton Archer') ],
                []))))"
         [/function]



Now, let's rate how good control we have over a location on the map. This function is currently REALLY simple! Just how many units of ours can reach it vs how many enemy units can.

Code:

         [function]
         name=rate_position_defensiveness
         inputs="ai*,src,dst"
         formula="units_can_reach(ai.my_moves, dst).size - units_can_reach(ai.enemy_moves, dst).size"
         [/function]


A utility function which, given an attack analysis object will give us the series of moves needed to carry out the attack:

Code:

            [function]
            name=build_attacks
            inputs=attack_move
            formula="map(attack_move.movements, attack(src, dst, attack_move.target))"
            [/function]


A function which will return a list of locations that we consider 'targets' -- i.e. places we want our units to get to. For the moment, very simple. Just all the villages that aren't ours.

Code:

         [function]
         name=targets
         inputs="ai"
         formula="ai.enemy_and_unowned_villages"
         [/function]


A function which will tell us the distance a location is from the nearest target. Generally the lower this number is the more we would want our units in this location, because it means they're nearer to a target.

Code:

         [function]
         name=distance_to_target
         inputs="ai,dst"
         formula="min(map(targets(ai), distance_between(dst, self)))"
         [/function]


A function which will find a movement toward a target by one of our units. This function is a little more complex. It tries to find the move which will move one of our units closest to a target. However, our leader doesn't move, and we don't move if the position is considered to be too bad defensively.

Code:

         [function]
         name=move_to_targets
         inputs=ai*
         formula="
           find(moves, src != my_leader and rate_position_defensiveness(ai, src, dst) > 0)
             where moves = sort(my_moves.moves, distance_to_target(ai, a.dst) < distance_to_target(ai, b.dst))"
         #  if(moves, choose(moves, -distance_to_target(ai, dst)), null())
         #    where moves = filter(my_moves.moves, src != my_leader and rate_position_defensiveness(ai, src, dst) > 0)
         [/function]


A function which gives us all the possible moves we can make which result in us capturing a village:

Code:

         [function]
         name=get_village_captures
         inputs="ai"
         formula="
           sum(map(ai.enemy_and_unowned_villages, 'village', map(units_can_reach(ai.my_moves, village), move(loc, village))), [])"
         [/function]


And a function which tells us captures which are 'uncontended': i.e. we can capture a village and no-one can attack us after we do:

Code:

         [function]
         name=uncontended_captures
         inputs=ai
         formula="filter(get_village_captures(ai), (src != ai.my_leader) and (units_can_reach(ai.enemy_moves, dst).empty))"
         [/function]



Finally the formula which determines what move we will make. First we try playing an opening move, if the opening() function returns moves still. Then we take any villages over which there is no contention, if any are available. Then we recruit Skeleton Archers and Dark Adepts.

If there are available attacks, we choose what we think is the best one. Finally any remaining units move toward targets.

Code:

            move="if(var.done_opening, [], opening(self) + [set_var('done_opening', 1)]) +
               if(uncontended_captures(self), [head(uncontended_captures(self))],
                  [ recruit('Skeleton Archer', loc(11,21)), recruit('Dark Adept', loc(11,22)) ] +
               if(size(attacks) > 0, build_attacks(choose(attacks, chance_to_kill*1000 + avg_damage_inflicted)), [move_to_targets(self) or 'end_turn'])
             
              )"
        [/ai]


This AI is pretty basic, of course, but so are the formulas used to create it! There are only a few formulas, and they are all very very basic. It's rather easy to create a basic AI using the formula system.

Also note that this is very 'pluggable'. You could take an AI like this and change just one function to totally revamp the behavior.
