Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

#!/usr/bin/env python3 

 

from .errors import * 

from .forums import ForumObserver 

 

class Actor(ForumObserver): 

 

def __init__(self): 

super().__init__() 

self.world = None 

self._forum = None 

self._id_factory = None 

 

def __rshift__(self, message): 

return self.send_message(message) 

 

def send_message(self, message): 

info("sending message: {message}") 

 

# Make sure the user didn't pass the wrong object to this function or  

# forget to call the superclass constructor. 

 

from .messages import require_message 

require_message(message) 

 

# Make sure this message hasn't been sent more than once. This is  

# conceptually dangerous because messages can accumulate state as they  

# are processed, and practically dangerous because it breaks the "sent  

# message cache" in multiplayer games. 

 

if message.was_sent(): 

raise ApiUsageError("""\ 

{message} has already been sent. 

 

It's not safe to send the same message twice because  

messages can accumulate state as they are executed.  

Duplicate messages would also break the system by which  

clients react to responses from the server in multiplayer  

games.""") 

 

# Make sure that every token referenced in this message (i.e. every  

# token that would get pickled when this message is sent over the  

# network) is either in or not in the world, whichever is expected: 

#  

# - Tokens that are being added to the world are expected to not  

# already be in it. 

# 

# - Any other token is expected to be in the world. 

#  

# These expectations can sometimes be broken by relatively innocuous  

# misuses of the game engine, so it's useful to have these checks. 

 

for token in message.tokens_referenced(): 

if token not in self.world and token not in message.tokens_to_add(): 

raise ApiUsageError("""\ 

{token} was referenced by {message} despite not  

being in the world. 

 

Every token referenced by a message, except those being  

added to the world, must be in the world. This is  

mostly a sanity check: a token that's not in the world  

shouldn't be in a message because it shouldn't be  

participating in the game at all! But this is also a  

synchronization issue for multiplayer games: there's no  

way to communicate over the network about a token that  

doesn't have an established id. 

 

There are several common ways to get this error:  

 

1. Using a token that was previously removed from the  

world. This can happen if a token is removed from  

the world but not from all the lists it was a part  

of, for example.  

 

2. Forgetting to yield a token from tokens_to_add().  

Even tokens that are nested in other tokens need to  

be yielded by this method to be added to the world. 

 

3. Using a token that was never added to the world.  

This can happen is you put a token in the world  

without using a message to do it.""") 

 

for token in message.tokens_to_add(): 

if token in self.world: 

raise ApiUsageError("""\ 

can't add {token} to the world twice. 

 

{token} was referenced by tokens_to_add(), but it can't  

be added to the world because it's already in it.""") 

 

for token in message.tokens_to_remove(): 

if token not in self.world: 

raise ApiUsageError("""\ 

can't remove {token} from the world twice. 

 

{token} was referenced by tokens_to_remove(), but it  

can't be removed from the world because it's not in  

it. This usually means {token} is being removed for a  

second time, perhaps due to a stale reference, but it  

could also mean {token} was never added to the world in  

the first place.""") 

 

# Indicate that the message was sent by this actor and give the message  

# a chance to assign id numbers to the tokens it's creating. This is  

# done before the message is checked so that the check can make sure  

# valid ids were assigned. 

 

message._set_sender_id(self._id_factory) 

message._assign_token_ids(self._id_factory) 

 

# Make sure that the message isn't requesting something that can't be  

# done. For example, make sure the players have enough resource when  

# they're trying to buy things. If the message fails the check, an  

# exception will be raised. 

 

message._check(self.world) 

 

# Hand the message off to the forum to be applied to the world and  

# relayed on to all the other actors (which may or may not be on  

# different machines). 

 

self._forum.execute_message(message) 

 

@property 

def id(self): 

assert self._id_factory is not None, "Actor does not have id." 

return self._id_factory.get() 

 

def is_referee(self): 

return isinstance(self, Referee) 

 

def on_setup_gui(self, gui): 

pass 

 

def on_start_game(self, num_players): 

pass 

 

def on_update_game(self, dt): 

pass # pragma: no cover 

 

def on_finish_game(self): 

pass 

 

def _set_world(self, world): 

assert self.world is None, "can't set world twice" 

self.world = world 

 

def _set_forum(self, forum, id_factory): 

assert self._id_factory is None, "Actor already has id." 

self._id_factory = id_factory 

 

assert self._forum is None, "Actor already has forum." 

self._forum = forum 

 

def _get_nested_observers(self): 

return (token.get_extension(self) 

for token in self.world if token.has_extension(self)) 

 

def _relay_message(self, message): 

pass 

 

 

class Referee(Actor): 

 

class Reporter: 

 

def __init__(self, referee): 

self.referee = referee 

self.is_finished_reporting = False 

 

def __enter__(self): 

return self 

 

def __exit__(self, exc_type, exc_value, traceback): 

self.is_finished_reporting = True 

 

def __rshift__(self, message): 

return self.send_message(message) 

 

def send_message(self, message): 

if self.is_finished_reporting: 

raise ApiUsageError("""\ 

{message.__class__.__name__} message sent using a stale  

reporter. 

 

This message is raised when the reporter provided to  

Token.report() is used after that method returns. This  

can only happen if you save a reference to the  

reporter, which you shouldn't do. This is a  

multiplayer synchronization issue. Because the same  

token should exist in the same state on every machine  

playing the game, tokens normally can't send messages  

or they would be prone to sending duplicate messages.  

The exception is Token.report(), which is guaranteed to  

be called only on the server. Token.report() is  

provided with a reporter object that can be used to  

send messages, but it's illegal to save a reference to  

the reporter and use it after Token.report() returns.  

Such a design would lead to bugs on the clients, which  

are never given reporter objects.""") 

else: 

return self.referee.send_message(message) 

 

def on_update_game(self, dt): 

with Referee.Reporter(self) as reporter: 

self.world.on_report_to_referee(reporter) 

for token in self.world: 

token.on_report_to_referee(reporter) 

 

def _set_forum(self, forum, id_factory): 

super()._set_forum(forum, id_factory) 

assert self.id == 1 

 

 

 

@debug_only 

def require_actor(object): 

require_instance(Actor(), object) 

 

@debug_only 

def require_actors(objects): 

for object in objects: 

require_actor(object)