• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Zielak / cardsGame / 8551819a-a76b-477c-a70d-f6e4453c885e

30 Jun 2024 01:04PM UTC coverage: 73.943% (+1.0%) from 72.988%
8551819a-a76b-477c-a70d-f6e4453c885e

push

circleci

web-flow
fix: Bot compound actions (#109)

* chore(server): path mapping

* fix: bunch of small changes

* test: fixes

* fix: comp action association

* feat: consider ready clients

* feat: first player is ready by default

* fix: issues around failed room start

710 of 1126 branches covered (63.06%)

Branch coverage included in aggregate %.

275 of 379 new or added lines in 57 files covered. (72.56%)

4 existing lines in 3 files now uncovered.

2630 of 3391 relevant lines covered (77.56%)

138.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.65
/packages/server/src/conditions/subjects.ts
1
import { isMapLike } from "@cardsgame/utils"
23✔
2

3
import type { QuerableProps } from "@/queries/types.js"
4
import type { State } from "@/state/state.js"
5
import { isChild } from "@/traits/child.js"
23✔
6
import { hasOwnership } from "@/traits/ownership.js"
23✔
7
import { isParent } from "@/traits/parent.js"
23✔
8
import { hasSelectableChildren } from "@/traits/selectableChildren.js"
23✔
9

10
import type { Player } from "../index.js"
11

12
import { throwError } from "./errors.js"
23✔
13
import { ConditionsContextBase } from "./types.js"
14
import {
23✔
15
  getFlag,
16
  getInitialSubject,
17
  getRef,
18
  resetPropDig,
19
  resetSubject,
20
  setFlag,
21
  setRef,
22
} from "./utils.js"
23

24
/**
25
 * Getters and methods which change subject
26
 */
27
class ConditionSubjects<
28
  Context extends ConditionsContextBase<S>,
29
  S extends State = Context["state"],
30
> {
31
  /**
32
   * Sets new subject. This can be anything.
33
   * @yields completely new subject, provided in the argument
34
   * @example
35
   * ```ts
36
   * test().set([1, 2, 3]).as("choices")
37
   * ```
38
   */
39
  set(newSubject: unknown): this {
40
    setFlag(this, "subject", newSubject)
216✔
41

42
    return this
216✔
43
  }
44

45
  /**
46
   * Looks for a child entity by their `props`, starting from current subject.
47
   *
48
   * @yields an entity, found by `QuerableProps` query
49
   * @example
50
   * ```ts
51
   * test().query({ name: "deck" }).not.empty()
52
   * ```
53
   */
54
  query(props: QuerableProps): this {
55
    // find new subject in current subject
56
    const parent =
57
      getFlag(this, "subject") === undefined
16!
58
        ? getFlag(this, "state")
59
        : getFlag(this, "subject")
60

61
    if (!isParent(parent)) {
16!
62
      throwError(
×
63
        this,
64
        `query(props) | current subject is not a parent: "${typeof parent}" = ${parent}`,
65
      )
NEW
66
      return
×
67
    }
68

69
    const newSubject = parent.query(props)
16✔
70

71
    setFlag(this, "subject", newSubject)
16✔
72

73
    resetPropDig(this)
16✔
74

75
    return this
16✔
76
  }
77

78
  /**
79
   * Allows you to change subject to one of the initial subjects.
80
   *
81
   * @example
82
   * ```ts
83
   * test().subject.entity.its("name").equals("mainDeck")
84
   * ```
85
   */
86
  get subject(): Record<keyof Context, this> {
87
    const subjects = getFlag(this, "initialSubjects")
3✔
88
    const subjectNames = Object.keys(subjects)
3✔
89
    const properties = subjectNames.reduce((descriptor, subjectName) => {
3✔
90
      descriptor[subjectName] = {
9✔
91
        get: () => {
92
          setFlag(this, "subject", subjects[subjectName])
3✔
93
          return this
3✔
94
        },
95
      }
96

97
      return descriptor
9✔
98
    }, {} as PropertyDescriptorMap)
99

100
    return Object.defineProperties(
3✔
101
      {} as Record<keyof Context, this>,
102
      properties,
103
    )
104
  }
105

106
  /**
107
   * Alias to `test().subject`
108
   */
109
  get $(): Record<keyof Context, this> {
110
    return this.subject
×
111
  }
112

113
  /**
114
   * Changes subject to a prop of its current subject.
115
   * @yields subject prop's value to be asserted. Will remember the reference to the object, so you can chain key checks
116
   * @example
117
   * ```ts
118
   * test().entity
119
   *   .its("propA").equals("foo")
120
   *   .and.its("propB").above(5)
121
   *
122
   * test().its("customMap").its("propA").equals(true)
123
   * ```
124
   */
125
  its(propName: string): this {
126
    setFlag(this, "propName", propName)
48✔
127
    setFlag(this, "currentParent", getFlag(this, "subject"))
48✔
128

129
    const currentParent = getFlag(this, "currentParent")
48✔
130

131
    if (isMapLike(currentParent)) {
48✔
132
      setFlag(this, "subject", currentParent.get(propName))
2✔
133
    } else {
134
      setFlag(this, "subject", currentParent[propName])
46✔
135
    }
136

137
    return this
48✔
138
  }
139

140
  /**
141
   * @yields parent of current subject
142
   */
143
  get parent(): this {
144
    const subject = getFlag(this, "subject")
3✔
145

146
    if (!isChild(subject)) {
3✔
147
      throwError(this, `parent | Subject is not a child.`)
1✔
NEW
148
      return
×
149
    }
150
    const parent = subject.parent
2✔
151

152
    if (!parent) {
2!
UNCOV
153
      throwError(this, `parent | Subject is the root state.`)
×
NEW
154
      return
×
155
    }
156
    setFlag(this, "subject", parent)
2✔
157

158
    return this
2✔
159
  }
160

161
  /**
162
   * @yields children of current subject (an array)
163
   */
164
  get children(): this {
165
    const parent = getFlag(this, "subject")
4✔
166

167
    if (!isParent(parent)) {
4✔
168
      throwError(this, `children | Current subject can't have children`)
1✔
NEW
169
      return
×
170
    }
171

172
    const children = parent.getChildren()
3✔
173

174
    setFlag(this, "subject", children)
3✔
175

176
    return this
3✔
177
  }
178

179
  /**
180
   * @yields a child at a specific index (of array or Parent)
181
   * @param index
182
   */
183
  nthChild(index: number): this {
184
    const subject = getFlag(this, "subject")
19✔
185

186
    if (isParent(subject)) {
19✔
187
      if (index > subject.countChildren() || index < 0) {
8✔
188
        throwError(this, `nthChild | Out of bounds`)
2✔
NEW
189
        return
×
190
      }
191

192
      setFlag(this, "subject", subject.getChild(index))
6✔
193
    } else if (Array.isArray(subject)) {
11✔
194
      if (index > subject.length || index < 0) {
7✔
195
        throwError(this, `nthChild | Out of bounds`)
2✔
196
      }
197

198
      setFlag(this, "subject", subject[index])
5✔
199
    } else if (typeof (subject as any).length === "number") {
4✔
200
      if (index > (subject as any).length || index < 0) {
3!
NEW
201
        throwError(this, `nthChild | Out of bounds`)
×
202
      }
203

204
      setFlag(this, "subject", subject[index])
3✔
205
    } else {
206
      throwError(this, `nthChild | Subject must be an array or Parent`)
1✔
207
    }
208

209
    return this
14✔
210
  }
211

212
  /**
213
   * @yields first element in collection
214
   */
215
  get bottom(): this {
216
    const subject = getFlag(this, "subject")
21✔
217

218
    if (typeof subject !== "object") {
21✔
219
      throwError(
4✔
220
        this,
221
        `bottom | Can't get the "bottom" of something other than an object`,
222
      )
223
    }
224

225
    if (isParent(subject)) {
17✔
226
      setFlag(this, "subject", subject.getBottom())
11✔
227
    } else if ("length" in (subject as any)) {
5!
228
      setFlag(this, "subject", subject[0])
5✔
229
    } else {
230
      throwError(this, `bottom | Couldn't decide how to get the "bottom" value`)
×
231
    }
232

233
    return this
16✔
234
  }
235

236
  /**
237
   * @yields last element in collection
238
   */
239
  get top(): this {
240
    const subject = getFlag(this, "subject") as { length: number }
20✔
241

242
    if (typeof subject !== "object") {
20✔
243
      throwError(
4✔
244
        this,
245
        `top | Can't get the "top" of something other than an object`,
246
      )
247
    }
248

249
    if (isParent(subject)) {
16✔
250
      setFlag(this, "subject", subject.getTop())
10✔
251
    } else if ("length" in subject) {
5!
252
      setFlag(this, "subject", subject[subject.length - 1])
5✔
253
    } else {
254
      throwError(this, `top | Couldn't decide how to get the "top" value`)
×
255
    }
256

257
    return this
15✔
258
  }
259

260
  /**
261
   * @yields {number} `length` property of a collection (or string)
262
   */
263
  get itsLength(): this {
264
    const subject = getFlag(this, "subject") as { length: number }
8✔
265

266
    if (subject.length === undefined) {
8✔
267
      throwError(this, `length | Subject doesn't have "length" property`)
1✔
268
    }
269

270
    setFlag(this, "subject", subject.length)
7✔
271
    return this
7✔
272
  }
273

274
  /**
275
   * @yields all selected children
276
   */
277
  get selectedChildren(): this {
278
    const subject = getFlag(this, "subject")
4✔
279

280
    if (!isParent(subject)) {
4✔
281
      throwError(this, `selectedChildren | Expected subject to be parent`)
1✔
NEW
282
      return
×
283
    }
284
    if (!hasSelectableChildren(subject)) {
3✔
285
      throwError(
1✔
286
        this,
287
        `selectedChildren | Subjects children are not selectable`,
288
      )
NEW
289
      return
×
290
    }
291

292
    setFlag(this, "subject", subject.getSelectedChildren())
2✔
293

294
    return this
2✔
295
  }
296

297
  /**
298
   * @yields all NOT selected children
299
   */
300
  get unselectedChildren(): this {
301
    const subject = getFlag(this, "subject")
4✔
302

303
    if (!isParent(subject)) {
4✔
304
      throwError(this, `unselectedChildren | Expected subject to be parent`)
1✔
NEW
305
      return
×
306
    }
307
    if (!hasSelectableChildren(subject)) {
3✔
308
      throwError(
1✔
309
        this,
310
        `unselectedChildren | Subjects children are not selectable`,
311
      )
NEW
312
      return
×
313
    }
314

315
    setFlag(this, "subject", subject.getUnselectedChildren())
2✔
316

317
    return this
2✔
318
  }
319

320
  /**
321
   * @yields {number} children count if `subject` is a `Parent`
322
   */
323
  get childrenCount(): this {
324
    const subject = getFlag(this, "subject")
1✔
325

326
    if (!isParent(subject)) {
1!
327
      throwError(this, `childrenCount | Expected subject to be a parent`)
×
NEW
328
      return
×
329
    }
330

331
    const count = subject.countChildren()
1✔
332
    setFlag(this, "subject", count)
1✔
333

334
    return this
1✔
335
  }
336

337
  /**
338
   * @yields {number} number of selected children if subject is parent
339
   */
340
  get selectedChildrenCount(): this {
341
    const subject = getFlag(this, "subject")
4✔
342

343
    if (!isParent(subject)) {
4✔
344
      throwError(this, `childrenCount | Expected subject to be parent`)
1✔
NEW
345
      return
×
346
    }
347
    if (!hasSelectableChildren(subject)) {
3✔
348
      throwError(this, `childrenCount | Subjects children are not selectable`)
1✔
NEW
349
      return
×
350
    }
351

352
    const count = subject.countSelectedChildren()
2✔
353
    setFlag(this, "subject", count)
2✔
354

355
    return this
2✔
356
  }
357

358
  /**
359
   * @yields {number} number of selected children if subject is parent
360
   */
361
  get unselectedChildrenCount(): this {
362
    const subject = getFlag(this, "subject")
4✔
363

364
    if (!isParent(subject)) {
4✔
365
      throwError(
1✔
366
        this,
367
        `unselectedChildrenCount | Expected subject to be parent`,
368
      )
NEW
369
      return
×
370
    }
371
    if (!hasSelectableChildren(subject)) {
3✔
372
      throwError(
1✔
373
        this,
374
        `unselectedChildrenCount | Subjects children are not selectable`,
375
      )
NEW
376
      return
×
377
    }
378

379
    const count = subject.countUnselectedChildren()
2✔
380
    setFlag(this, "subject", count)
2✔
381

382
    return this
2✔
383
  }
384

385
  /**
386
   * @yields {number} value of `idx` of the entity
387
   */
388
  get idx(): this {
389
    const subject = getFlag(this, "subject")
8✔
390

391
    if (!isChild(subject)) {
8✔
392
      throwError(this, `idx | Expected subject to be a child`)
4✔
393
    } else {
394
      setFlag(this, "subject", subject.idx)
4✔
395
    }
396

397
    return this
4✔
398
  }
399

400
  /**
401
   * @yields {number} value of `selectionIndex` of the entity
402
   */
403
  get selectionIndex(): this {
404
    const subject = getFlag(this, "subject")
9✔
405

406
    if (!isChild(subject)) {
9✔
407
      throwError(this, `selectionIndex | Expected subject to be a child`)
4✔
408
    } else if (!hasSelectableChildren(subject.parent)) {
5✔
409
      throwError(
1✔
410
        this,
411
        `selectionIndex | Parent without ability to select children`,
412
      )
413
    } else {
414
      setFlag(this, "subject", subject.parent.getSelectionIndex(subject.idx))
4✔
415
    }
416

417
    return this
4✔
418
  }
419

420
  /**
421
   * **REQUIRES** `"player"` initial subject
422
   *
423
   * Changes subject to owner of current entity
424
   * @yields `player`
425
   */
426
  get owner(): this {
427
    const subject = getFlag(this, "subject")
×
428

429
    if (!isChild(subject) || !hasOwnership(subject)) {
×
430
      throwError(this, `owner | Expected subject to be ownable child`)
×
431
    } else {
432
      setFlag(this, "subject", subject.owner)
×
433
    }
434

435
    return this
×
436
  }
437

438
  /**
439
   * **REQUIRES** `"player"` initial subject
440
   *
441
   * Changes subject to entity of current dragging happening.
442
   * @yields `entity`
443
   */
444
  get playersDraggedEntity(): this {
445
    const player = getInitialSubject<Player>(this, "player")
×
446

447
    if (!player) {
×
448
      throwError(this, `playersDraggedEntity | Expected player in event`)
×
449
    } else {
450
      setFlag(
×
451
        this,
452
        "subject",
453
        getInitialSubject<Player>(this, "player").dragStartEntity,
454
      )
455
    }
456

457
    return this
×
458
  }
459

460
  /**
461
   * Bring back previously remembered subject by its alias
462
   * @param alias
463
   *
464
   * @example
465
   * ```ts
466
   * test().query({ name: "deck" }).as("deck")
467
   * // ...
468
   * test().get("deck").children.not.empty()
469
   * ```
470
   */
471
  get(alias: string): this {
472
    const newSubject = getRef(this, alias)
13✔
473

474
    if (!newSubject) {
13✔
475
      throwError(this, `get | There's nothing under "${alias}" alias`)
1✔
476
    }
477

478
    setFlag(this, "subject", newSubject)
12✔
479

480
    return this
12✔
481
  }
482

483
  /**
484
   * Remembers subject found by queryProps with a given alias.
485
   * Won't start looking for (querying) new subject if given
486
   * alias is already populated with something (for performance!).
487
   *
488
   * @example
489
   * ```ts
490
   * test().remember("deck", { name: "deck" })
491
   * ```
492
   */
493
  remember(alias: string, props: QuerableProps): void {
494
    const currentAliasValue = getRef(this, alias)
2✔
495

496
    if (currentAliasValue) {
2!
497
      // TODO: have _refs remember some query props in addition, so at least we have another way of comparing
498
      return
×
499
    }
500

501
    // find new subject in current subject
502
    const currentSubject = getFlag(this, "subject")
2✔
503
    const parent =
504
      currentSubject === undefined || !isParent(currentSubject)
2!
505
        ? (getFlag(this, "state") as State)
506
        : currentSubject
507

508
    setRef(this, alias, parent.query(props))
2✔
509
  }
510

511
  /**
512
   * Remembers current subject with a given alias
513
   * @example
514
   * ```ts
515
   * test().as({ name: "deck" }).as("deck")
516
   * ```
517
   */
518
  as(alias: string): void {
519
    setRef(this, alias, getFlag(this, "subject"))
8✔
520

521
    resetSubject(this)
8✔
522
  }
523
}
524

525
export { ConditionSubjects }
23✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc