lzrtag-base 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +98 -0
- data/docs/EventSymbols.md +117 -0
- data/docs/MQTTBranches.md +74 -0
- data/lib/lzrtag.rb +13 -0
- data/lib/lzrtag/game/base_game.rb +220 -0
- data/lib/lzrtag/handler/base_handler.rb +169 -0
- data/lib/lzrtag/handler/count_handler.rb +55 -0
- data/lib/lzrtag/handler/game_handler.rb +258 -0
- data/lib/lzrtag/handler/hitArb_handler.rb +80 -0
- data/lib/lzrtag/hooks/base_hook.rb +143 -0
- data/lib/lzrtag/hooks/standard_hooks.rb +174 -0
- data/lib/lzrtag/map/map_set.rb +48 -0
- data/lib/lzrtag/map/map_zone.rb +95 -0
- data/lib/lzrtag/map/myMaps_parser.rb +136 -0
- data/lib/lzrtag/player/base_player.rb +104 -0
- data/lib/lzrtag/player/effects_player.rb +96 -0
- data/lib/lzrtag/player/hardware_player.rb +251 -0
- data/lib/lzrtag/player/life_player.rb +116 -0
- metadata +131 -0
@@ -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
|