Main
Download
For developers
Perkun sessions

Main page

The NPCs

In the Perkun Wars there are three NPCs - Dorban, Pregor and Thragos. Each of the NPCs is running in a separate process, communicating with the main process through pipes. Each NPC process is running a Perkun interpreter (instantiating a class inherited from perkun::optimizer_with_all_data).

Dorban is a witcher, he is controlled by the file dorban_general.perkun. Pregor is a human, he is controlled by the file pregor_general.perkun. Thragos is also a human, he is controlled by the file thragos_general.perkun. These files can be found in the folder perkun/final_code. They are generated with Prolog.

Payoff functions

Please compare the payoff functions for these three NPCs. This is the payoff function for Dorban:

payoff
{
set({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>true}, 100.0);
set({where_is_Dorban=>place_Shadizar, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Shadizar, do_I_see_vampire=>true}, 100.0);
set({where_is_Dorban=>place_Novigrad, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Novigrad, do_I_see_vampire=>true}, 100.0);
}
Note that Dorban is getting 100.0 points when he can see the vampire. This is the reason why Dorban is hunting the vampire! Perkun attempts to maximize the expected value of the payoff function.

Take a look at the payoff function for Pregor:

payoff
{
set({where_is_Pregor=>place_Wyzima, do_I_see_vampire=>false}, 100.0);
set({where_is_Pregor=>place_Wyzima, do_I_see_vampire=>true}, 0.0);
set({where_is_Pregor=>place_Shadizar, do_I_see_vampire=>false}, 100.0);
set({where_is_Pregor=>place_Shadizar, do_I_see_vampire=>true}, 0.0);
set({where_is_Pregor=>place_Novigrad, do_I_see_vampire=>false}, 100.0);
set({where_is_Pregor=>place_Novigrad, do_I_see_vampire=>true}, 0.0);
}
Note that Pregor is getting 100.0 points when he cannot see the vampire. This is the reason why Pregor is avoiding the vampire. The same holds for Thragos:
payoff
{
set({where_is_Thragos=>place_Wyzima, do_I_see_vampire=>false}, 100.0);
set({where_is_Thragos=>place_Wyzima, do_I_see_vampire=>true}, 0.0);
set({where_is_Thragos=>place_Shadizar, do_I_see_vampire=>false}, 100.0);
set({where_is_Thragos=>place_Shadizar, do_I_see_vampire=>true}, 0.0);
set({where_is_Thragos=>place_Novigrad, do_I_see_vampire=>false}, 100.0);
set({where_is_Thragos=>place_Novigrad, do_I_see_vampire=>true}, 0.0);
}

Therefore there are two NPCs avoiding the vampire and one NPC hunting it. When you attack a vampire and there are some NPCs around - they are going to help you. The best strategy is to wait with the humans for the vampire. When he comes - attack him (the humans will help you). Hunting the vampire without any help is a bad strategy.

Take a look at the file src/perkun_wars.cc. It creates the pipes and then forks to three child processes that communicate with the main process through these pipes.

Take a look at the file inc/perkun_wars.h. You will find a class inherited from perkun::optimizer_with_all_data. The instance of this class is running the Perkun interpreter.

class npc: public perkun::optimizer_with_all_data
{
...
}

How is the final code created

You can see in the folder perkun/initial_code the initial specifications for Dorban (dorban_general.perkun), Pregor (pregor_general.perkun) and Thragos (thragos_general.perkun). The initial code contains only values and variables. The payoff function and the model are empty! For example perkun/initial_code/dorban_general.perkun is:

values
{
	value false, true; # logical values
	value place_Wyzima,place_Shadizar,place_Novigrad; # places
	value do_nothing,escape,fight,goto_Wyzima,goto_Shadizar,goto_Novigrad; # actions
}

variables
{
	input variable where_is_Dorban:{place_Wyzima,place_Shadizar,place_Novigrad}; # where am I
	input variable do_I_see_vampire:{false, true};
	output variable action:{do_nothing,goto_Wyzima,goto_Shadizar,goto_Novigrad}; # actions
	hidden variable where_is_vampire:{place_Wyzima,place_Shadizar,place_Novigrad};
}


payoff
{
}

model
{
}

cout << prolog generator << eol;
We have two input variables, one output variable and one hidden variable here. This file is used by Perkun to create a Prolog generator. The Prolog generator for Dorban (see the folder perkun/prolog) is initially dorban_general.prolog, but later is enhanced with certain rules (by the Perl script perkun/perl/inject_dorban_general_rules.pl) and the final version for Dorban is perkun/prolog/dorban_general_final.prolog. This Prolog code can be used to produce perkun/final_code/dorban_general.perkun! See the perkun/Makefile.am to trace the dependencies. You will need SWI Prolog to run these Prolog codes.

So initially we have Perkun code with just values and variables, then we produce a Prolog generator, enhance it with certain rules and execute, to produce the final Perkun code. Take a look at the file perkun/perl/inject_dorban_general_rules.pl. The script is looking in your Prolog generator for the comments like % PLEASE INSERT YOUR CODE HERE-get_probability and replaces them with some rules. The same is done for % PLEASE INSERT YOUR CODE HERE-write_model_impossible_if_necessary, % PLEASE INSERT YOUR CODE HERE-write_model_action_illegal_if_necessary and % PLEASE INSERT YOUR CODE HERE-get_payoff.

get_payoff

Maybe the payoff is the easiest one. Take a look at perkun/prolog/dorban_general.prolog. Just before the comment % PLEASE INSERT YOUR CODE HERE-get_payoff you will find a comment: % get_payoff(INPUT_where_is_Dorban,INPUT_do_I_see_vampire, PAYOFF). This is intended to help you to formulate the get_payoff rules. The script inject_dorban_general_rules.pl replaces the comment with the fact "get_payoff(_,true, 100.0)":

s/% PLEASE INSERT YOUR CODE HERE-get_payoff/

get_payoff(_,true, 100.0). % Dorban is hunting the vampires
/g;
This means that no matter where Dorban is whenever INPUT_do_I_see_vampire is true the PAYOFF equals 100.0. The final code for Dorban contains the payoff generated by these rules:
payoff
{
set({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>true}, 100.0);
set({where_is_Dorban=>place_Shadizar, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Shadizar, do_I_see_vampire=>true}, 100.0);
set({where_is_Dorban=>place_Novigrad, do_I_see_vampire=>false}, 0.0);
set({where_is_Dorban=>place_Novigrad, do_I_see_vampire=>true}, 100.0);
}

get_probability

In the file perkun/prolog/dorban_general.prolog jusr before the comment % PLEASE INSERT YOUR CODE HERE-get_probability there is the comment % get_probability(INITIAL_where_is_Dorban, INITIAL_do_I_see_vampire, INITIAL_where_is_vampire, ACTION_action, TERMINAL_where_is_Dorban, TERMINAL_do_I_see_vampire, TERMINAL_where_is_vampire, PROBABILITY).. The placeholders here are:

  • INITIAL_where_is_Dorban
  • INITIAL_do_I_see_vampire
  • INITIAL_where_is_vampire
  • ACTION_action
  • TERMINAL_where_is_Dorban
  • TERMINAL_do_I_see_vampire
  • TERMINAL_where_is_vampire
  • PROBABILITY
Take a look at one of the rules we inserted here:
get_probability(INITIAL_where_is_Dorban, _, INITIAL_where_is_vampire, goto_Wyzima, place_Wyzima, TERMINAL_do_I_see_vampire, INITIAL_where_is_vampire, 1.0):-
	not(INITIAL_where_is_Dorban = place_Wyzima),
	see_variable_consistent_with_place(TERMINAL_do_I_see_vampire,INITIAL_where_is_vampire,place_Wyzima),
	write('# Dorban is going to Wyzima'), nl.
It means that if Dorban is not in Wyzima (initially) and tries to get there (goto_Wyzima) then with 100% probability he will get there and will see the vampire or not, depending on where the vampire currently is. Note that we use INITIAL_where_is_vampire also to match TERMINAL_where_is_vampire, because the action goto_Wyzima does not change the location where the vampire is. The predicate see_variable_consistent_with_place is an auxilliary predicate added to check whether the vampire is in Wyzima or not.

write_model_impossible_if_necessary

There are two rules injected for the predicate write_model_impossible_if_necessary:

write_model_impossible_if_necessary(INITIAL_where_is_Dorban, false, INITIAL_where_is_Dorban):-
	write('impossible({where_is_Dorban=>'),
	write(INITIAL_where_is_Dorban),
	write(', do_I_see_vampire=>false, where_is_vampire=>'), 
	write(INITIAL_where_is_Dorban), write('}); # Dorban must see vampire'), nl.
	
write_model_impossible_if_necessary(INITIAL_where_is_Dorban, true, INITIAL_where_is_vampire):-
	not(INITIAL_where_is_Dorban = INITIAL_where_is_vampire),
	write('impossible({where_is_Dorban=>'),
	write(INITIAL_where_is_Dorban),
	write(', do_I_see_vampire=>true, where_is_vampire=>'), 
	write(INITIAL_where_is_vampire), write('}); # Dorban cannot see vampire'), nl.
The first one generates for example the Perkun code:
impossible({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>false, where_is_vampire=>place_Wyzima}); # Dorban must see vampire
It means that if both Dorban and the vampire are in Wyzima then it is impossible for Dorban not to see the vampire.
The second one for example:
impossible({where_is_Dorban=>place_Shadizar, do_I_see_vampire=>true, where_is_vampire=>place_Wyzima}); # Dorban cannot see vampire
This means that if Dorban is in Shadizar and the vampire is in Wyzima then it is impossible for Dorban to see the vampire.

write_model_action_illegal_if_necessary

Similarly we write the Perkun command "illegal" within the model if some actions cannot be performed in some situations. For example the code:

write_model_action_illegal_if_necessary(place_Wyzima, SEE, goto_Wyzima):-
	write('illegal({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>'), 
	write(SEE), write('},{action=>goto_Wyzima});'), nl.
... generates Perkun code:
illegal({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>false},{action=>goto_Wyzima});
illegal({where_is_Dorban=>place_Wyzima, do_I_see_vampire=>true},{action=>goto_Wyzima});
... and it means that if Dorban is in Wyzima then he cannot perform the action goto_Wyzima (no matter whether he can see the vampire or not).

virtual functions in class npc

In order to communicate with the main process through the pipes we need to override some virtual functions and implement them so that on input we read from one pipe (from parent) and on output we write to another pipe (to parent). These virtual functions are get_input and execute. We must also handle the situation when an NPC gets surprised (for example he expected to see vampire somewhere, but he didn't, since the vampire himself moved). This surprise is handled by the function on_error_in_populate_belief_for_consequence. In this game we handle the surprises by setting the current belief to a uniform distribution. For example if Dorban moves to Wyzima and does not see the vampire there, but expected to see him, then he will assume that with 50% probability the vampire is in Novigrad and 50% with Shadizar.

In the Perkun Wars one can talk with the NPCs (chat) - i.e. find out what they think/believe. This is done so that a special command "chat" is passed as input and the NPC generates the response based on his former belief. Also the optimal action is converted to an explanation (for example do_nothing to the text "I am staying here ..."). Take a look at the file npc.cc to see the implementation details.

npc_proxy

There is a special class used to represent an NPC in the main process. It is called npc_proxy. It uses the pipes to talk with the child process.