11# Authors: Ondrej Lukas - ondrej.lukas@aic.fel.cvut.cz
22from os import path , makedirs
3+ from typing import Optional , Tuple
34import numpy as np
45import random
56import pickle
67import argparse
78import logging
9+ from datetime import datetime
810
911from netsecgame import Action , GameState , BaseAgent , generate_valid_actions , state_as_ordered_string
1012from netsecgame .game_components import AgentRole
13+ from netsecgame .utils .trajectory_recorder import TrajectoryRecorder
1114
1215class SARSAAgent (BaseAgent ):
1316
@@ -36,7 +39,7 @@ def get_state_id(self, state:GameState) -> int:
3639 self ._str_to_id [state_str ] = len (self ._str_to_id )
3740 return self ._str_to_id [state_str ]
3841
39- def select_action (self , state :GameState , testing = False ) -> Action :
42+ def select_action (self , state :GameState , testing = False ) -> Tuple [ Action , int ] :
4043 actions = generate_valid_actions (state )
4144 state_id = self .get_state_id (state )
4245
@@ -55,9 +58,11 @@ def select_action(self, state:GameState, testing=False) -> Action:
5558 self .q_values [state_id , action ] = 0
5659 return action , state_id
5760
58- def play_episode (self , testing = False )-> list :
61+ def play_episode (self , testing = False , recorder : Optional [ TrajectoryRecorder ] = None , filename = None )-> list :
5962 observation = self .request_game_reset ()
6063 episodic_returns = []
64+ if recorder is not None :
65+ recorder .add_initial_state (observation .state )
6166 action1 ,state_id1 = self .select_action (observation .state , testing )
6267 done = observation .end
6368 while not done :
@@ -71,13 +76,18 @@ def play_episode(self, testing=False)->list:
7176 if not testing :
7277 self .q_values [state_id1 , action1 ] += self .alpha * (observation2 .reward + self .gamma * self .q_values [state_id2 , action2 ]- self .q_values [state_id1 , action1 ])
7378
79+ if recorder is not None :
80+ recorder .add_step (action1 , observation2 .reward , observation2 .state )
7481 # move1 step
7582 action1 = action2
7683 state_id1 = state_id2
7784 done = observation2 .end
85+ if recorder is not None :
86+ recorder .save_to_file (filename = filename )
87+ recorder .reset ()
7888 return episodic_returns
7989
80- def play_game (self , num_episodes = 1 , testing = False ):
90+ def play_game (self , num_episodes = 1 , testing = False , recorder : TrajectoryRecorder = None ):
8191 """
8292 The main function for the gameplay. Handles agent registration and the main interaction loop.
8393 """
@@ -90,13 +100,21 @@ def play_game(self, num_episodes=1, testing=False):
90100 if episode and episode % args .eval_each == 0 :
91101 testing_returns = []
92102 for _ in range (args .eval_for ):
93- testing_returns .append (np .sum (self .play_episode (testing = True )))
103+ if recorder :
104+ filename = filename = f"{ datetime .now ():%Y-%m-%d} _SARSA_Attacker_{ episode :06d} "
105+ testing_returns .append (np .sum (self .play_episode (testing = True , recorder = recorder , filename = filename )))
106+ else :
107+ testing_returns .append (np .sum (self .play_episode (testing = True )))
94108 self ._logger .info (f"Eval after { episode } episodes: ={ np .mean (testing_returns )} ±{ np .std (testing_returns )} " )
95109 if episode % args .store_models_every == 0 and episode != 0 :
96- self .store_q_table (f'sarsa_agent_marl.experiment{ args .experiment_id } -episodes-{ episode } .pickle' )
110+ self .store_q_table (f'sarsa_agent_marl.experiment{ args .experiment_id } -episodes-{ episode :06d } .pickle' )
97111 returns = []
98112 for _ in range (args .eval_for ):
99- returns .append (np .sum (self .play_episode (testing = True )))
113+ if recorder :
114+ filename = filename = f"{ datetime .now ():%Y-%m-%d} _SARSA_Attacker_{ num_episodes :06d} "
115+ returns .append (np .sum (self .play_episode (testing = True , recorder = recorder , filename = filename )))
116+ else :
117+ returns .append (np .sum (self .play_episode (testing = True )))
100118 self ._logger .info (f"Final results for { self .__class__ .__name__ } after { num_episodes } episodes: { np .mean (returns )} ±{ np .std (returns )} " )
101119 self ._logger .info ("Terminating interaction" )
102120 self .terminate_connection ()
@@ -116,6 +134,7 @@ def play_game(self, num_episodes=1, testing=False):
116134 parser .add_argument ("--experiment_id" , help = "Id of the experiment to record into Mlflow." , default = 'sarsa_006_coordinatorV3' , type = str )
117135 parser .add_argument ("--store_models_every" , help = "Store a model to disk every these number of episodes." , default = 2000 , type = int )
118136 parser .add_argument ("--previous_model" , help = "Store a model to disk every these number of episodes." , type = str )
137+ parser .add_argument ("--record_trajectories" , type = bool , default = False )
119138 args = parser .parse_args ()
120139
121140 if not path .exists (args .logdir ):
@@ -124,11 +143,14 @@ def play_game(self, num_episodes=1, testing=False):
124143
125144 # Create agent
126145 agent = SARSAAgent (args .host , args .port , alpha = args .alpha , gamma = args .gamma , epsilon = args .epsilon )
127-
146+ if args .record_trajectories :
147+ recorder = TrajectoryRecorder ("SARSA" , agent_role = "Attacker" )
148+ else :
149+ recorder = None
128150 if args .test_only :
129151 agent .load_q_table (args .previous_model )
130- agent .play_game (args .episodes , testing = True )
152+ agent .play_game (args .episodes , testing = True , recorder = recorder )
131153 else :
132- agent .play_game (args .episodes , testing = False )
154+ agent .play_game (args .episodes , testing = False , recorder = recorder )
133155 agent .store_q_table ("./sarsa_agent_marl.pickle" )
134156
0 commit comments