-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathJIRA.txt
352 lines (271 loc) · 17 KB
/
JIRA.txt
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
## Cleanup Work
[ ] Get rid of front-end module, make it a single module project
[ ] Remove the idea of an "opponent", games can have anywhere from 1-6 players
[ ] (think) get rid of CardId?
[ ] Separate the Application layer from the Domain
[ ] Move PORT to application
[ ] Split out card shuffle (RandomCardShuffler) into PORT and ADAPTER
[ ] Pull in ArchUnit
## Game Layout
Per Game:
* TILEs
* ACTION CARD DECK
* ACTION CARD DISCARD PILE
* TEST RESULTS DECK
* TEST RESULTS DISCARD PILE
* 6-SIDED DIE
Per Player:
* PAWN (indicates which TILE they're on)
* HAND
* PLAY AREA
* TECH NEGLECT AREA
* COMMIT TRACKER
* RISK TRACKER
Player's "State Machine":
Player's "TURN" is complete when either: play/discard 3 cards OR explicitly END TURN
## Board Layout
1. One large images vs. individual Hex Tiles: leaning towards individual tiles, easier to determine pawn placement and more flexible
### Hex Tiles
Workspace1: belongs to a single player, or group of players
exit(Workspace1): moves pawn from Tile 1 to Tile 2
exit(Workspace1): moves pawn from Tile 2 to Tile 3
exit(Workspace1): moves pawn from Tile 3 to Tile 4
skip(Workspace1): moves pawn from tile 4 to Tile 7
Ensemble Mode: Player <4:1> Workspace <1..4:1> Tile
Pairing Mode: Player <2:1> Workspace <1..4:1> Tile
Individual Mode: Player <1:1> Workspace <1..4:1> Tile
Board has Map<WorkspaceId, BoardTile>, BoardTile is an Enum? Record? There are two "types" of tiles: those with Exit-Only and those with Exit+Skip
?? Do we want a reverse multi-map? <BoardTile, List<WorkspaceId>>
Exit: via Discard, play Write Code card, or Successful Prediction
Skip: Tile has "canSkip()" query and throws exception if "skip()" for tile that can't be skipped. Otherwise skip() associates the new Tile with the Player
### Pawns
* Associate players with their pawns via colors (except when in Ensemble Game Mode, then use a separate color?
?? In Ensemble Mode: Does the pawn retain its own color, or does it take on the color of the current "Driver" (player whose turn it is)
* Turn indicator
* ?? Where to place pawn tokens on each hex tile: define 4 locations where pawns can appear
* [Later] Tap/click on item (hex tile or card) to zoom in
* Player-Pawn is 1:1, except when playing as a group/cooperative game ("Ensemble Game Mode")
## Need
[ ] Structure for the BOARD as 9 separate TILEs,
[ ] PLAYER is ON a TILE (has a game state machine)
[ ] player's COMMAND causes change, potentially transition to new TILE
[ ] MOVE PAWN - moves the player to a new tile
[ ] DRAW CARD (and DRAW FULL)
[ ] DISCARD CARD (often causes a transition)
[ ] PLAY CARD
[ ] END TURN
## Ubiquitous Language
* Game (deck, card, tile, etc.)
* Competitive: every member for themselves (1:1 player:pawn)
* Co-op (mob programming): working together to move pawn forward (N:1 player:pawn)
* Player - ties an Account/Member to a specific game ("player in game")
* Host - part of a Team, they created an instance of a game
* Team - which is an "account-based" thing: who can "see" each other's games and join
* Group - set of players currently playing a specific game (they have all JOINed a specific GAME or are members of the game)
* one/many PAWN <-> one/many PLAYER (premature)
* Support variation where GROUP controls single PAWN
* Coach - can observe the game without playing, can control/manipulate game elements
* "Attached" to one or more TEAMs
* Win Condition (when is the game over)
* First to 5 commits (or 3 commits, whatever)
* Timeboxed: most commits within a period of time (duration)
* Game Master view: can see state of game(s) that have been created/in-progress
* Admin version of this allows manipulation of elements for any reason (not sure if this is different from coach role)
* Admin can see any game any time
* Coach can only see games for the TEAMs to which they're attached
## Behavior
1. Host Creates Game (defines competitive or co-op)
2. Player Joins (existing) Game
* Invited by host, or
* Chooses available game from team/group
* If Host wants to play, they have to join too
3. Start Game
* Decks are shuffled
* Cards are dealt: 5 to each Player
* Pawns placed at first TILE
* Commit tracker set to 0
* Risk tracker set to 0/0
* Starting player chosen (proceeds clockwise)
4. Player 1 plays...
## Interaction
[X] 1. Ask for name for new game (start with competitive)
[X] 2. Create game -> provide identifier/link (handle)
[X] 3. Member joins game with human-readable "Handle" (via typing it in, not link)
[X] Member becomes Player when joined the game
[X] Multiple different Members can join the same game
[X] Constraint: A Member can not join the same game twice, i.e., can't represent TWO Players
* If a Member joins a game where they are already a player, that is "rejoining" (reconnecting)
[X] Constraint: 4 players maximum per game (rejoins don't count, of course)
[ ] 4. Waiting for game to start (once at least 1 member has joined the game)
[X] Receive notification when additional players are added
[ ] Can only start game at this point by calling .start()
[ ] 5. "start game" is Triggered
[ ] Add guard to ensure game can't be started unless at least 1 player has joined
[ ] Action Card deck shuffled, 5 cards dealt to each Player, etc.
[X] Create ActionCard, Deck container, able to draw and replenish from discard
[X] Nullable-ize the Deck with Embedded Stub for configuring response to "give me shuffled deck"
[X] Player draws to full hand from Action Card deck as part of start game
[X] 5 events generated: 1 for each of 5 cards drawn
[X] State of player's Hand: has 5 cards
[X] Deck event-sourcing
[X] Draw (with no replenish) generates events
[X] Draw event projects state change in Deck
[X] Replenish generates events
[X] Applying replenish to deck
[X] Player discards card, which adds to Discard Pile
[ ] Display the hex board with pawns for each player
[X] The association of Member (and their logged-in username) vs. the Player's name in the context of a Game needs to be resolved. When we're sending Player-tailored HTML, we need to know: for which `Player` we're generating the HTML (we know this because we iterate through all Players from the Game, i.e., game.players()), and THEN, we need to send that HTML to the correct WebSocketSession, which has to be associated (mapped) with the Game + PlayerId.
[X] PlayerConnections needs to have a sendTo(GameId, PlayerId, HTML): the Game that the Player is in, used to identify the target websocket to which we send the customized HTML
When we get a connection through the WebSocketInboundHandler, we need to know which game this is from (we get this via the "join:gameHandle" message), and we ALSO NEED the PlayerId (we get this via the security principal's name)
[X] PlayerConnector will collaborate with the new ForTrackingPlayerMessageSenders Port interface (which is implemented by the MessageSendersForPlayers class)
[ ] Start Game starts the game:
[X] The modal goes away (via broadcasting `hx-swap-oob="delete"`)
[ ] If the game is not already started, call .start() (and persist this state change)
[ ] Implement this from the GameTest
[X] Broadcast Players' cards (WebSocketBroadcaster.gameUpdate)
[X] Player has cards in the "Your Hand" area
[X] Deal cards to all players
[X] Shuffle the ActionCardDeck
[X] Show other players' cards in their areas:
[X] Update game.html to only have the <div class="all-other-players-container">, which will be empty, and filled in only once the game starts
(This is because when the game.html loads, we don't yet know how many players are in the game)
[X] In WebSocketBroadcaster, when we do a gameUpdate(), send the same "other players' cards" HTML content to everyone
[X] Convert PlayerViewComponentTest to be structural instead of raw string comparisons
[X] Display cards in "Your Hand" as Images instead of words
[X] Display cards in Other Players' hands as Images instead of words
[ ] HTML element hierarchy cleanup
[ ] Stop using Text HtmlComponent for H2 and other tags, by creating a generic HtmlTag component, and then escape the text
[ ] Move childComponents down to NormalElement (only NormalElement can have children)
[ ] Popup menu for Card actions
[X] Generalize NormalElement to accept arbitrary list of HtmlAttributes
[X] Change "your hand" cards from DIV to BUTTON
[X] Add htmx support to hand BUTTON:
hx-get="/game/{game handle}/card-menu/{card name}"
hx-on::after-settle="document.querySelector('dialog').showModal()"
hx-swap="none"
[X] Implement GET request that returns HTMX:
@GetMapping("/game/{gameHandle}/card-menu/{cardName}")
@ResponseBody
"""
<swap hx-swap-oob="innerHTML" id="dialog">
<div>
<button autofocus
hx-post="/game/%s/cards/play/%s"
hx-on::before-request="document.querySelector('dialog').close()">
Play Card into Workspace
</button>
</div>
<div>
<button hx-post="/game/%s/cards/discard/%s"
hx-on::before-request="document.querySelector('dialog').close()">
Discard Card to Discard Pile
</button>
</div>
</swap>
""".formatted(gameHandle, cardName, gameHandle, cardName);
[X] Implement POST request to handle DISCARD command
[X] Create POST endpoint
[X] Wire the endpoint to the Use Case command
[X] Broadcast updated hand view now that the card has been discarded
[X] Display Action Card deck draw and discard piles
[X] HTML+CSS for the draw/discard piles
[X] Broadcast deck state changes to everyone
[X] DRAW PILE: Display a back of card if it's not empty
<swap id="action-card-draw-pile"> <img src="/action-card-back.png"> </swap>
[X] DISCARD PILE: Display most recently discarded card if it's not empty
<swap id="action-card-discard-pile"> <img src=discarded card> </swap>
[X] If either pile is empty, the <swap> will be empty
[X] Refreshing the page should trigger a refresh of the state from the back-end
[X] Should happen upon the Websocket's "handleMessage" "connect" command
[X] Kick user back to the lobby if they have a stale game page
==> WE ARE HERE:
[ ] Handle pawn placement
[X] Figure out how to display pawn in/on correct tile
[ ] Pawn Icon: Use hexagon with player's number inside, or allow player to pick a "pawn" from a set of icons
[X] At start of game, set all pawns to first hex tile via the Workspace
[ ] in Game#createPlayer(), create a Workspace and pass it into the Player's constructor (Workspace's ID matches the Player's ID--until we get to Workspace sharing)
[X] In Workspace constructor, assign the WhatShouldItDo[HexTile] to its currentHexTile field
[ ] Render Workspace: for each workspace:
[X] Delete (any existing) pawn: swap="delete" for id="workspace1-pawn" (or whatever the workspace ID is)
[X] Render the pawn on the currentHexTile (e.g., id="what-should-it-do-hex-tile"), do a swap with "beforeend"
[ ] Render using the "real" Workspace object instead of faking it with Player's ID
[ ] Create Workspace+Pawn+Scoring+Risk
[ ] Discard card causes Pawn to move
[ ] Implement POST request to handle PLAY card command
[ ] Implement the PLAY command
[ ] Show played cards in the Workspace
[ ] Player might have cards in the "Tech Neglect" area
[ ] Player has "In-Play" area (for WRITE CODE, LESS CODE, PREDICT)
[ ] Draw Action Card from the deck's draw pile
[ ] If it's a "tech neglect" card, move it to Workspace
[ ] Remove Tailwind
[X] Handle WebSocket disconnect
[X] Handle reconnection and re-joining of the game
[ ] When player disconnects, show the "disconnected" overlay on that player's container
[ ] Show a "toast" message when they disconnect
[ ] Show a toast when they're reconnected
[ ] Show a "connected" status indicator in the player's container
[ ] Reorganize the ViewComponents to have a more direct relationship to where their contents appear on the page
[ ] HtmlElement.Forest currently indents its children, but it shouldn't as the Forest isn't really a container element
[ ] Replace List<HtmlAttribute> with HtmlAttributes that can render itself (fixes Primitive [Collection] Obsession)
e.g.: HtmlAttributes.id(targetId).hxSwap("afterbegin")
[ ] Split the Broadcaster interface into two:
(1) All events before the game has started (e.g., announce player joined)
(2) All events while the game is being played
[ ] Maybe split further into: events during START vs. events while game is being played, or put the "clear modal" into the pre-start game interface
[ ] (Temporary) Constraint: players can't join once game is started
[X] Inbound HTTP Adapter
[X] Host create game: numeric-only textbox and "Create Game" button does POST
[X] MVC part
[X] Test to make sure hostNewGame has Game created and stored
[X] Lobby shows games available with handles for each
[X] Person join game: textbox (for handle) and "Join Game" button does POST
[X] MVC test for /join endpoint
[X] PostMapping that does:
[X] Creates Player in the Game for the Person
[X] Redirects to page that shows the game page
[X] Game-in-progress page: show "modal"
[X] Show other players as they join -- via WebSockets (htmx)
[X] Associate logged-in user (username?) with a MemberId for joining, by looking up that username in MemberStore
[X] Get first name from the Player (their in-game name)
[X] Show "Start Game" button (might have constraints on when this button shows up later)
[X] Create standard action deck when starting the game
[ ] Outbound (Notifier) WebSocket Adapter
[ ] Display name of game, all players, decks, pawns, etc.
## Event-Sourcing Game Persistence
>>> Commands -> Events -> State
[X] EventSourcedAggregate<GameEvent>
[X] Static method for creating initial game
[X] GameEvent interface, with subclasses for each event type
[X] Fix the Game.join() method to not return the Player, but instead provide method to obtain the Player based on the PersonId
Using in-memory persistence for Game
[X] Use case to use load (from Repository), modify, save (to Repository) pattern
[X] GameStore in-memory implementation
[X] Make it use the event-source storage model (so freshEvents is clear upon reconstitution, i.e., when findBy happens)
[ ] GameViewLoader implementation using the GameRepository
Real Database
[ ] Truly unique ID for Game (instead of Game's "handle")
[ ] Repository Adapter for JDBC
[ ] JDBC concrete implementation
## Tooling
[ ] Add HexArchTest
[ ] Add rule that Production code must not call nullable methods (createNull) and only call constructors
[ ] Create custom AssertJ assertions, e.g.:
[ ] Find a specific event in a list of events and assert on its properties
e.g.:
```
assertThat(game.freshEvents())
.forEvent(ActionCardDeckCreated.class)
.hasOne()
.extracting(ActionCardDeckCreated::actionCards)
.hasSize(63);
```
## Naming
[X] Come up with a name other than Person(Id) that's easier to differentiate from Player(Id), hopefully something other than "Account" or "User"
Perhaps "Member"?
## Future Features
[ ] Person's own page showing their profile, active games they're in, past games they've played, etc.
## Open Questions
[ ] Do we allow non-Players to see the Game page?
[ ] If so, who is allowed to observe?
[ ] If they can, they must not be able to click on any buttons, etc.