lzrtag-base 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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