lzrtag-base 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+
2
+ require 'mqtt/sub_handler'
3
+
4
+ require_relative '../hooks/base_hook.rb'
5
+
6
+ require_relative '../player/life_player.rb'
7
+
8
+ module LZRTag
9
+ module Handler
10
+ # The base handler class.
11
+ # This class code deals with the most rudimentary systems:
12
+ # - It handles the MQTT connection
13
+ # - It registers new players and distributes MQTT data to the respective class
14
+ # - It hands out Player IDs to connected players
15
+ # - It runs the event loop system and manages hooks
16
+ #
17
+ # In it's simplest form it can be instantiated with
18
+ # just a MQTT handler:
19
+ # @example
20
+ # # Using LZRTag.Handler instead of LZRTag::Handler::Base to fetch the latest handler
21
+ # handler = LZRTag.Handler.new(mqttConn);
22
+ class Base
23
+ # Returns the MQTT connection
24
+ attr_reader :mqtt
25
+
26
+ # Returns the ID-Table, a Hash of Players and their matched IDs
27
+ attr_reader :idTable
28
+
29
+ def initialize(mqtt, playerClass = Player::Life, clean_on_exit: true)
30
+ @mqtt = mqtt;
31
+
32
+ @playerClass = playerClass;
33
+
34
+ @players = Hash.new();
35
+ @idTable = Hash.new();
36
+
37
+ @playerSynchMutex = Mutex.new();
38
+
39
+ @hooks = [self];
40
+ @eventQueue = Queue.new();
41
+
42
+ @eventThread = Thread.new do
43
+ loop do
44
+ nextData = @eventQueue.pop;
45
+ @hooks.each do |h|
46
+ h.consume_event(nextData[0], nextData[1]);
47
+ end
48
+ end
49
+ end
50
+ @eventThread.abort_on_exception = true;
51
+
52
+ @mqtt.subscribe_to "Lasertag/Players/#" do |data, topic|
53
+ dID = topic[0];
54
+ if(not @players.key? dID)
55
+ @playerSynchMutex.synchronize {
56
+ @players[dID] = @playerClass.new(dID, self);
57
+ }
58
+ send_event(:playerRegistered, @players[dID]);
59
+ end
60
+
61
+ @players[dID].on_mqtt_data(data, topic);
62
+ end
63
+
64
+ if(clean_on_exit)
65
+ at_exit {
66
+ @playerSynchMutex.synchronize {
67
+ @players.each do |id, player|
68
+ player.clear_all_topics();
69
+ sleep 0.1;
70
+ end
71
+ }
72
+ sleep 0.5;
73
+ }
74
+ end
75
+
76
+ puts "I LZR::Handler init finished".green
77
+ end
78
+
79
+ # Send an event into the event loop.
80
+ # Any events will be queued and will be executed in-order by a separate
81
+ # thread. The provided data will be passed along to the hooks
82
+ # @param evtName [Symbol] Name of the event
83
+ # @param *data Any additional data to send along with the event
84
+ def send_event(evtName, *data)
85
+ raise ArgumentError, "Event needs to be a symbol!" unless evtName.is_a? Symbol;
86
+ @eventQueue << [evtName, data];
87
+ end
88
+
89
+ # @private
90
+ def consume_event(evtName, data)
91
+ case evtName
92
+ when :playerConnected
93
+ player = data[0];
94
+ i = 1;
95
+ while(@idTable[i]) do i+=1; end
96
+ @idTable[i] = player;
97
+ player.id = i;
98
+ when :playerDisconnected
99
+ player = data[0];
100
+ @idTable[player.id] = nil;
101
+ player.id = nil;
102
+ end
103
+ end
104
+
105
+ # Add or instantiate a new hook.
106
+ # This function will take either a Class of Hook::Base or an
107
+ # instance of it, and add it to the current list of hooks, thusly
108
+ # including it in the event processing
109
+ # @param hook [LZRTag::Hook::Base] The hook to instantiate and add
110
+ # @return The added hook
111
+ def add_hook(hook)
112
+ hook = hook.new(self) if hook.is_a? Class and hook <= LZRTag::Hook::Base;
113
+
114
+ unless(hook.is_a? LZRTag::Hook::Base)
115
+ raise ArgumentError, "Hook needs to be a Lasertag::EventHook!"
116
+ end
117
+
118
+ return if(@hooks.include? hook);
119
+ hook.on_hookin(self);
120
+ @hooks << hook;
121
+
122
+ return hook;
123
+ end
124
+ # Remove an existing hook from the system.
125
+ # This will remove the provided hook instance from the event handling
126
+ # @param hook [LZRTag::Hook::Base] The hook to remove
127
+ def remove_hook(hook)
128
+ unless(hook.is_a? Lasertag::EventHook)
129
+ raise ArgumentError, "Hook needs to be a Lasertag::EventHook!"
130
+ end
131
+
132
+ return unless @hooks.include? hook
133
+ hook.on_hookout();
134
+ @hooks.delete(hook);
135
+ end
136
+
137
+ # Return a player either by their ID or their DeviceID
138
+ # @return LZRTag::Player::Base
139
+ def [](c)
140
+ return @players[c] if c.is_a? String
141
+ return @idTable[c] if c.is_a? Integer
142
+
143
+ raise ArgumentError, "Unknown identifier for the player id!"
144
+ end
145
+ alias get_player []
146
+
147
+ # Run the provided block on each registered player.
148
+ # @param connected Only yield for connected players
149
+ # @yield [player] Yields for every played. With connected = true,
150
+ # only yields connected players
151
+ def each(connected: false)
152
+ @playerSynchMutex.synchronize {
153
+ @players.each do |_, player|
154
+ yield(player) if(player.connected? | !connected);
155
+ end
156
+ }
157
+ end
158
+
159
+ # Returns the number of currently connected players
160
+ def num_connected()
161
+ n = 0;
162
+ self.each_connected do
163
+ n += 1;
164
+ end
165
+ return n;
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,55 @@
1
+
2
+ require_relative 'hitArb_handler.rb'
3
+ require_relative '../player/hardware_player.rb'
4
+
5
+ module LZRTag
6
+ module Handler
7
+ # This class provides useful statistics about the current game and situation.
8
+ #
9
+ # The various readable attributes contain various information on the game, such as
10
+ # current kill count, damage done, team and brightness composition, etc.
11
+ # Most of this information can be used to determine game progress
12
+ class Count < HitArb
13
+ # Returns a Hash with keys 0..7, describing which teams have
14
+ # how many players
15
+ attr_reader :teamCount
16
+ # Returns a Hash with keys equal to player's brightnesses, describing
17
+ # how many players have which brightness
18
+ attr_reader :brightnessCount
19
+
20
+ def initialize(*args, **argHash)
21
+ super(*args, **argHash);
22
+
23
+ @teamCount = Hash.new();
24
+ 7.times do |i|
25
+ @teamCount[i] = 0;
26
+ end
27
+
28
+ @brightnessCount = Hash.new();
29
+ Player::Hardware.getBrightnessKeys().each do |bKey|
30
+ @brightnessCount[bKey] = 0;
31
+ end
32
+ end
33
+
34
+ # @private
35
+ def consume_event(evtName, data)
36
+ super(evtName, data);
37
+
38
+ case evtName
39
+ when :playerRegistered
40
+ @teamCount[data[0].team] += 1;
41
+ @brightnessCount[data[0].brightness] += 1;
42
+ when :playerUnregistered
43
+ @teamCount[data[0].team] -= 1;
44
+ @brightnessCount[data[0].brightness] -= 1;
45
+ when :playerTeamChanged
46
+ @teamCount[data[1]] -= 1;
47
+ @teamCount[data[0].team] += 1;
48
+ when :playerBrightnessChanged
49
+ @brightnessCount[data[1]] -= 1;
50
+ @brightnessCount[data[0].brightness] += 1;
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,258 @@
1
+
2
+ require_relative 'count_handler.rb'
3
+
4
+ module LZRTag
5
+ module Handler
6
+ =begin
7
+ Game handler class, managing game registration and game ticks.
8
+ This class manages the lifecycle of any lasertag game started in it.
9
+ This includes sending a gameTick, managing phase and game state transitions,
10
+ sending out but also receiving information on the game state via MQTT, etc.
11
+
12
+ @example
13
+ handler = LZRTag::Handler::Game.new(mqtt);
14
+
15
+ handler.register_game("My Game", SomeGame);
16
+ handler.register_game("Other game!", SomeOtherGame);
17
+
18
+ handler.start_game(SomeGame); # This will fetch the registered name and publish to MQTT
19
+ # Alternatively, sending to Lasertag/Game/Controls/SetGame with
20
+ # the string name will switch to the game.
21
+
22
+ # Game and phase switching is possible at any time, and will be handled asynchronously
23
+ sleep 5;
24
+ handler.set_phase(:somePhase);
25
+ =end
26
+ class Game < Count
27
+ # Returns the instance of the currently active game, or nil if none is present
28
+ attr_reader :currentGame
29
+
30
+ # currently active game phase
31
+ # When set it will start a phase change, sending :gamePhaseEnds and
32
+ # :gamePhaseStarts events
33
+ # @see set_phase
34
+ # @return Symbol
35
+ attr_reader :gamePhase
36
+
37
+ # List of in-game players.
38
+ # This list is mainly to keep track of which players the game is acting
39
+ # upon, but updating it will also send a list of player ID Strings to
40
+ # MQTT (Lasertag/Game/ParticipatingPlayers), and will send :playerEnteredGame
41
+ # and :playerLeftGame events
42
+ # @return Array<LZRTag::Player::Base>
43
+ attr_reader :gamePlayers
44
+
45
+ def initialize(*data, **argHash)
46
+ super(*data, **argHash)
47
+
48
+ @lastTick = Time.now();
49
+
50
+ @lastGame = nil;
51
+ @currentGame = nil;
52
+ @nextGame = nil;
53
+
54
+ @gamePhase = :idle;
55
+
56
+ @gamePlayers = Array.new();
57
+
58
+ @knownGames = Hash.new();
59
+
60
+ _start_game_thread();
61
+
62
+ @mqtt.subscribe_to "Lasertag/Game/Controls/+" do |data, topics|
63
+ case topics[0]
64
+ when "SetPhase"
65
+ phase = data.to_sym;
66
+ if(get_allowed_phases().include? phase)
67
+ set_phase(phase);
68
+ end
69
+ when "SetGame"
70
+ if(@knownGames[data])
71
+ start_game(@knownGames[data])
72
+ elsif(data == "STOP")
73
+ stop_game();
74
+ end
75
+ end
76
+ end
77
+
78
+ clean_game_topics();
79
+ at_exit {
80
+ clean_game_topics();
81
+ }
82
+ end
83
+
84
+ def clean_game_topics()
85
+ @mqtt.publish_to "Lasertag/Game/ParticipatingPlayers", [].to_json(), retain: true
86
+ @mqtt.publish_to "Lasertag/Game/KnownGames", [].to_json, retain: true;
87
+ @mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
88
+ end
89
+ private :clean_game_topics;
90
+
91
+ def _start_game_thread()
92
+ @gameTickThread = Thread.new() do
93
+ loop do
94
+ Thread.stop() until(@nextGame.is_a? LZRTag::Game::Base);
95
+
96
+ @currentGame = @nextGame;
97
+ set_phase(:starting);
98
+
99
+ @lastTick = Time.now();
100
+ while(@currentGame == @nextGame)
101
+ sleep @currentGame.tickTime
102
+ dT = Time.now() - @lastTick;
103
+ @lastTick = Time.now();
104
+
105
+ send_event(:gameTick, dT);
106
+ end
107
+
108
+ puts "Stopping current game.".green
109
+ set_phase(:idle);
110
+ sleep 1;
111
+ @currentGame = nil;
112
+ end
113
+ end
114
+ @gameTickThread.abort_on_exception = true;
115
+ end
116
+ private :_start_game_thread;
117
+
118
+ # Register a game by a tag.
119
+ # This function will register a given LZRTag::Game::Base class under a given
120
+ # string tag. This tag can then be used to, via MQTT, start the game, and is
121
+ # also used to give players a cleartext game name.
122
+ # A list of games is published to Lasertag/Game/KnownGames
123
+ # @param gameTag [String] Cleartext name of the game
124
+ # @param game [LZRTag::Game::Base] The game class to register
125
+ def register_game(gameTag, game)
126
+ raise ArgumentError, "Game Tag must be a string!" unless gameTag.is_a? String
127
+ raise ArgumentError, "Game must be a LZRTag::Game class" unless game <= LZRTag::Game::Base
128
+
129
+ @knownGames[gameTag] = game;
130
+
131
+ @mqtt.publish_to "Lasertag/Game/KnownGames", @knownGames.keys.to_json, retain: true;
132
+ end
133
+
134
+ # @private
135
+ def consume_event(event, data)
136
+ super(event, data)
137
+
138
+ return unless @currentGame
139
+ @currentGame.consume_event(event, data);
140
+ end
141
+
142
+ # Starts a given new game (or the last one).
143
+ # This function will take either a String (as registered with register_game),
144
+ # or a LZRTag::Game::Base class, instantiate it, and start it.
145
+ # If no fitting game was found, the game is instead stopped.
146
+ # @param game [String,LZRTag::Game::Base] The game, or game name, to start
147
+ def start_game(game = @lastGame)
148
+ @lastGame = game;
149
+
150
+ if(game.is_a? String and gClass = @knownGames[game])
151
+ game = gClass;
152
+ elsif(game.is_a? String)
153
+ stop_game();
154
+ return;
155
+ end
156
+
157
+ if(gKey = @knownGames.key(game))
158
+ @mqtt.publish_to "Lasertag/Game/CurrentGame", gKey, retain: true
159
+ puts "Starting game #{gKey}!".green
160
+ else
161
+ @mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
162
+ end
163
+
164
+ game = game.new(self) if game.is_a? Class and game <= LZRTag::Game::Base;
165
+ unless(game.is_a? LZRTag::Game::Base)
166
+ raise ArgumentError, "Game class needs to be specified!"
167
+ end
168
+ @nextGame = game;
169
+ send_event(:gameStarting, @nextGame);
170
+
171
+ @gameTickThread.run();
172
+ @mqtt.publish_to "Lasertag/Game/Phase/Valid",
173
+ get_allowed_phases.to_json(), retain: true
174
+ end
175
+
176
+ # Stops the currently running game.
177
+ def stop_game()
178
+ @nextGame = nil;
179
+ @mqtt.publish_to "Lasertag/Game/CurrentGame", "", retain: true
180
+ end
181
+
182
+ # Returns an Array<Symbol> of the currently allowed phases of this game.
183
+ # This list can also be retrieved via MQTT, under Lasertag/Game/CurrentGame
184
+ def get_allowed_phases()
185
+ allowedPhases = [:idle]
186
+ if(@currentGame)
187
+ allowedPhases = [allowedPhases, @currentGame.phases].flatten
188
+ end
189
+
190
+ return allowedPhases;
191
+ end
192
+
193
+ # Tries to change the current phase.
194
+ # This function will set the current phase to nextPhase, if it is an allowed
195
+ # one. However, if nextPhase does not belong to the list of allowed phases,
196
+ # and error is raised. The :gamePhaseEnds and :gamePhaseStarts events are
197
+ # triggered properly. This function can be called from any context, not just
198
+ # inside the game code itself.
199
+ # @see get_allowed_phases
200
+ def set_phase(nextPhase)
201
+ allowedPhases = get_allowed_phases();
202
+
203
+ raise ArgumentError, "Phase must be valid!" unless allowedPhases.include? nextPhase
204
+
205
+ puts "Phase started: #{nextPhase}!".green;
206
+
207
+ oldPhase = @gamePhase
208
+ send_event(:gamePhaseEnds, oldPhase, nextPhase)
209
+
210
+ @mqtt.publish_to "Lasertag/Game/Phase/Current", @gamePhase.to_s, retain: true
211
+ @gamePhase = nextPhase;
212
+ send_event(:gamePhaseStarts, nextPhase, oldPhase);
213
+ end
214
+ # Alias for set_phase
215
+ # @see set_phase
216
+ def gamePhase=(nextPhase)
217
+ set_phase(nextPhase)
218
+ end
219
+
220
+ # Update the list of in-game players.
221
+ # @param [Array<LZRTag::Player::Base] Array of active players
222
+ def gamePlayers=(newPlayers)
223
+ raise ArgumentError, "Game player list shall be an array!" unless newPlayers.is_a? Array
224
+ @gamePlayers = newPlayers.dup;
225
+
226
+ @playerNames = Array.new();
227
+ plNameArray = Array.new();
228
+ @gamePlayers.each do |pl|
229
+ plNameArray << pl.deviceID();
230
+ end
231
+
232
+ newPlayers = @gamePlayers - @oldGamePlayers;
233
+ newPlayers.each do |pl|
234
+ send_event :playerEnteredGame, pl;
235
+ end
236
+ oldPlayers = @oldGamePlayers - @gamePlayers;
237
+ oldPlayers.each do |pl|
238
+ send_event :playerLeftGame, pl;
239
+ end
240
+
241
+ @oldGamePlayers = @gamePlayers.dup;
242
+ @mqtt.publish_to "Lasertag/Game/ParticipatingPlayers", plNameArray.to_json(), retain: true
243
+ end
244
+
245
+ # Yield for each currently in-game player
246
+ def each_participating()
247
+ @gamePlayers.each do |pl|
248
+ yield(pl)
249
+ end
250
+ end
251
+
252
+ # Check if a player is currently in game
253
+ def in_game?(player)
254
+ return @gamePlayers.include? player
255
+ end
256
+ end
257
+ end
258
+ end