Smart Contract
FastBreakV1
A.0b2a3299cc857e29.FastBreakV1
1/*
2 _____ __ ___. __
3 _/ ____\____ _______/ |_ \_ |_________ ____ _____ | | __
4 \ __\\__ \ / ___/\ __\ | __ \_ __ \_/ __ \\__ \ | |/ /
5 | | / __ \_\___ \ | | | \_\ \ | \/\ ___/ / __ \| <
6 |__| (____ /____ > |__| |___ /__| \___ >____ /__|_ \
7 \/ \/ \/ \/ \/ \/
8
9 fast break game contract & oracle
10
11*/
12
13import NonFungibleToken from 0x1d7e57aa55817448
14import TopShot from 0x0b2a3299cc857e29
15import MetadataViews from 0x1d7e57aa55817448
16import TopShotMarketV3, Market from 0xc1e4f4f4c4257510
17
18/// Game & Oracle Contract for Fast Break V1
19///
20access(all) contract FastBreakV1: NonFungibleToken {
21
22 access(all) entitlement Play
23 access(all) entitlement Create
24 access(all) entitlement Update
25
26 /// Contract events
27 ///
28
29 access(all) event FastBreakPlayerCreated(
30 id: UInt64,
31 playerName: String
32 )
33
34 access(all) event FastBreakRunCreated(
35 id: String,
36 name: String,
37 runStart: UInt64,
38 runEnd: UInt64,
39 fatigueModeOn: Bool
40 )
41
42 access(all) event FastBreakRunStatusChange(id: String, newRawStatus: UInt8)
43
44 access(all) event FastBreakGameCreated(
45 id: String,
46 name: String,
47 fastBreakRunID: String,
48 submissionDeadline: UInt64,
49 numPlayers: UInt64
50 )
51
52 access(all) event FastBreakGameStatusChange(id: String, newRawStatus: UInt8)
53
54 access(all) event FastBreakNFTBurned(id: UInt64, serialNumber: UInt64)
55
56 access(all) event FastBreakGameTokenMinted(
57 id: UInt64,
58 fastBreakGameID: String,
59 serialNumber: UInt64,
60 mintingDate: UInt64,
61 topShots: [UInt64],
62 mintedTo: UInt64
63 )
64
65 access(all) event FastBreakGameSubmissionUpdated(
66 playerId: UInt64,
67 fastBreakGameID: String,
68 topShots: &[UInt64],
69 )
70
71 access(all) event FastBreakGameWinner(
72 playerId: UInt64,
73 submittedAt: UInt64,
74 fastBreakGameID: String,
75 topShots: &[UInt64]
76 )
77
78 access(all) event FastBreakGameStatAdded(
79 fastBreakGameID: String,
80 name: String,
81 type: UInt8,
82 valueNeeded: UInt64
83 )
84
85 /// Named Paths
86 ///
87 access(all) let CollectionStoragePath: StoragePath
88 access(all) let CollectionPublicPath: PublicPath
89 access(all) let OracleStoragePath: StoragePath
90 access(all) let PlayerStoragePath: StoragePath
91
92 /// Contract variables
93 ///
94 access(all) var totalSupply: UInt64
95 access(all) var nextPlayerId: UInt64
96
97 /// Game Enums
98 ///
99
100 /// A game of Fast Break has the following status transitions
101 ///
102 access(all) enum GameStatus: UInt8 {
103 access(all) case SCHEDULED /// Game is schedules but closed for submission
104 access(all) case OPEN /// Game is open for submission
105 access(all) case STARTED /// Game has started
106 access(all) case CLOSED /// Game is over and rewards are being distributed
107 }
108
109 /// A Fast Break Run has the following status transitions
110 ///
111 access(all) enum RunStatus: UInt8 {
112 access(all) case SCHEDULED
113 access(all) case RUNNING /// The first Fast Break game of the run has started
114 access(all) case CLOSED /// The last Fast Break game of the run has ended
115 }
116
117 /// A Fast Break Statistic can be met by an individual or group of top shots
118 ///
119 access(all) enum StatisticType: UInt8 {
120 access(all) case INDIVIDUAL /// Each top shot must meet or exceed this statistical value
121 access(all) case CUMMULATIVE /// All top shots in the submission must meet or exceed this statistical value
122 }
123
124 /// Metadata Dictionaries
125 /// Player mappings remain in contract storage (bounded by number of users)
126 /// Contract dictionaries
127 ///
128 access(self) let fastBreakRunByID: {String: FastBreakRun}
129 access(self) let fastBreakGameByID: {String: FastBreakGame}
130 access(self) let fastBreakPlayerByID: {UInt64: PlayerData}
131 access(self) let playerAccountMapping: {UInt64: Address}
132 access(self) let accountPlayerMapping: {Address: UInt64}
133
134 /// A top-level Fast Break Run, the container for Fast Break Games
135 /// A Fast Break Run contains many Fast Break games & is a mini-season.
136 /// Fatigue mode applies submission limitations for the off-chain version of the game
137 /// Fatigue mode limits top shot usage by tier. 4 uses legendary. 2 uses rare. 1 use other.
138 ///
139 access(all) struct FastBreakRun {
140 access(all) let id: String /// The off-chain uuid of the Fast Break Run
141 access(all) let name: String /// The name of the Run (R0, R1, etc)
142 access(all) var status: FastBreakV1.RunStatus /// The status of the run
143 access(all) let runStart: UInt64 /// The block timestamp starting the run
144 access(all) let runEnd: UInt64 /// The block timestamp ending the run
145 access(all) let runWinCount: {UInt64: UInt64} /// win count by playerId
146 access(all) let fatigueModeOn: Bool /// Fatigue mode is a game rule limiting usage of top shots by tier
147
148 init (id: String, name: String, runStart: UInt64, runEnd: UInt64, fatigueModeOn: Bool) {
149 self.id = id
150 self.name = name
151 self.status = FastBreakV1.RunStatus.SCHEDULED
152 self.runStart = runStart
153 self.runEnd = runEnd
154 self.runWinCount = {}
155 self.fatigueModeOn = fatigueModeOn
156 }
157
158 /// Update status of the Fast Break Run
159 ///
160 access(contract) fun updateStatus(status: FastBreakV1.RunStatus) { self.status = status }
161
162 /// Write a new win to the Fast Break Run runWinCount
163 ///
164 access(contract) fun incrementRunWinCount(playerId: UInt64) {
165 self.runWinCount[playerId] = (self.runWinCount[playerId] ?? 0) + 1
166 }
167 }
168
169 /// Get a Fast Break Run by Id (returns reference to avoid copying)
170 /// Returns read-only reference - avoids copying large structs
171 ///
172 access(all) view fun getFastBreakRun(id: String): &FastBreakV1.FastBreakRun? {
173 return &FastBreakV1.fastBreakRunByID[id]
174 }
175
176 /// A single Game of Fast Break
177 /// A Fast Break is played on any day NBA games are scheduled
178 /// It is the intention of this contract to allow private & public Fast Break games
179 /// A private Fast Break is visible on-chain but is restricted to private accounts
180 /// A public Fast Break can be played by custodial and non-custodial accounts
181 ///
182 access(all) struct FastBreakGame {
183 access(all) let id: String /// The off-chain uuid of the Fast Break
184 access(all) let name: String /// The name of the Fast Break (eg FB0, FB1, FB2)
185 access(all) var submissionDeadline: UInt64 /// The block timestamp restricting submission to the Fast Break
186 access(all) let numPlayers: UInt64 /// The number of top shots a player should submit to the Fast Break
187 access(all) var status: FastBreakV1.GameStatus /// The game status
188 access(all) var winner: UInt64 /// The playerId of the winner of Fast Break
189 access(all) var submissions: {UInt64: FastBreakV1.FastBreakSubmission} /// Map of player submission to the Fast Break
190 access(all) let fastBreakRunID: String /// The off-chain uuid of the Fast Break Run containing this Fast Break
191 access(all) var stats: [FastBreakStat] /// The NBA statistical requirements for this Fast Break
192
193 init (
194 id: String,
195 name: String,
196 fastBreakRunID: String,
197 submissionDeadline: UInt64,
198 numPlayers: UInt64
199 ) {
200 self.id = id
201 self.name = name
202 self.submissionDeadline = submissionDeadline
203 self.numPlayers = numPlayers
204 self.status = FastBreakV1.GameStatus.SCHEDULED
205 self.submissions = {}
206 self.fastBreakRunID = fastBreakRunID
207 self.stats = []
208 self.winner = 0
209 }
210
211 /// Get a account's active Fast Break Submission (returns reference to avoid copying)
212 ///
213 access(all) view fun getFastBreakSubmissionByPlayerId(playerId: UInt64): &FastBreakV1.FastBreakSubmission? {
214 return &self.submissions[playerId]
215 }
216
217 /// Add a statistic to the Fast Break during game creation
218 ///
219 access(contract) fun addStat(stat: FastBreakV1.FastBreakStat) {
220 self.stats.append(stat)
221 }
222
223 /// Set the submission deadline for a Fast Break
224 ///
225 access(contract) fun setSubmissionDeadline(deadline: UInt64) {
226 self.submissionDeadline = deadline
227 }
228
229 /// Update status and winner of a Fast Break
230 ///
231 access(contract) fun update(status: FastBreakV1.GameStatus, winner: UInt64) {
232 self.status = status
233 self.winner = winner
234 }
235
236 /// Submit a Fast Break
237 ///
238 access(contract) fun submitFastBreak(submission: FastBreakV1.FastBreakSubmission) {
239 pre {
240 FastBreakV1.isValidSubmission(submissionDeadline: self.submissionDeadline) : "Submission missed deadline"
241 }
242
243 self.submissions[submission.playerId] = submission
244 }
245
246 /// Update a Fast Break with new topshot moments
247 ///
248 access(contract) fun updateFastBreakTopshots(playerId: UInt64, topshotMoments: [UInt64]) {
249 pre {
250 FastBreakV1.isValidSubmission(submissionDeadline: self.submissionDeadline) : "Submission update missed deadline"
251 }
252
253 let submission = &self.submissions[playerId] as &FastBreakV1.FastBreakSubmission?
254 ?? panic("Could not find submission for playerId: ".concat(playerId.toString()))
255
256 submission.updateTopshots(topshotMomentIds: topshotMoments)
257 }
258
259 /// Update the Fast Break score of an account
260 ///
261 access(contract) fun updateScore(playerId: UInt64, points: UInt64, win: Bool): Bool {
262 let submission: FastBreakV1.FastBreakSubmission = self.submissions[playerId]
263 ?? panic("Unable to find fast break submission for playerId: ".concat(playerId.toString()))
264
265 let isPrevSubmissionWin = submission.win
266
267 submission.setPoints(points: points, win: win)
268
269 self.submissions[playerId] = submission
270
271 if win && !isPrevSubmissionWin {
272 return true
273 }
274
275 return false
276 }
277 }
278
279 /// Validate Fast Break Submission
280 ///
281 access(all) view fun isValidSubmission(submissionDeadline: UInt64): Bool {
282 return submissionDeadline > UInt64(getCurrentBlock().timestamp) + 60
283 }
284
285 /// Get a Fast Break Game by Id (returns reference to avoid copying large structs)
286 /// Returns read-only reference - avoids copying 1MB+ game structs with many submissions
287 ///
288 access(all) view fun getFastBreakGame(id: String): &FastBreakV1.FastBreakGame? {
289 return &FastBreakV1.fastBreakGameByID[id]
290 }
291
292 /// Get the game stats of a Fast Break (returns reference to avoid copying)
293 ///
294 access(all) view fun getFastBreakGameStats(id: String): &[FastBreakV1.FastBreakStat]? {
295 if let gameRef = FastBreakV1.getFastBreakGame(id: id) {
296 // gameRef.stats is already a reference when accessed through a reference
297 return gameRef.stats
298 }
299 return nil
300 }
301
302 /// Get a Fast Break account by playerId
303 ///
304 access(all) view fun getFastBreakPlayer(id: UInt64): Address? {
305 return FastBreakV1.playerAccountMapping[id]
306 }
307
308 /// A statistical structure used in Fast Break Games
309 /// This structure names the NBA statistic top shots must match or exceed
310 /// An example is points as the statistic and 30 as the value
311 /// A top shot or group of top shots must meet or exceed 30 points
312 ///
313 access(all) struct FastBreakStat {
314 access(all) let name: String
315 access(all) let type: FastBreakV1.StatisticType
316 access(all) let valueNeeded: UInt64
317
318 init (
319 name: String,
320 type: FastBreakV1.StatisticType,
321 valueNeeded: UInt64
322 ) {
323 self.name = name
324 self.type = type
325 self.valueNeeded = valueNeeded
326 }
327 }
328
329 /// An account submission to a Fast Break
330 ///
331 access(all) struct FastBreakSubmission {
332 access(all) let playerId: UInt64
333 access(all) var submittedAt: UInt64
334 access(all) let fastBreakGameID: String
335 access(all) var topShots: [UInt64]
336 access(all) var points: UInt64
337 access(all) var win: Bool
338
339 init (
340 playerId: UInt64,
341 fastBreakGameID: String,
342 topShots: [UInt64],
343 ) {
344 self.playerId = playerId
345 self.fastBreakGameID = fastBreakGameID
346 self.topShots = topShots
347 self.submittedAt = UInt64(getCurrentBlock().timestamp)
348 self.points = 0
349 self.win = false
350 }
351
352 /// Set the points of a submission
353 ///
354 access(contract) fun setPoints(points: UInt64, win: Bool) {
355 self.points = points
356 self.win = win
357 }
358
359 access(contract) fun updateTopshots(topshotMomentIds: [UInt64]) {
360 self.topShots = topshotMomentIds
361 }
362 }
363
364 /// Resource for playing Fast Break
365 /// The Fast Break Player plays the game & mints game tokens
366 ///
367 access(all) resource Player: FastBreakPlayer, NonFungibleToken.NFT {
368
369 access(all) let id: UInt64
370 access(all) let playerName: String /// username
371 access(all) var tokensMinted: UInt64 /// num games played
372
373 access(contract) var gameTokensPlayed: [UInt64]
374
375 init(playerName: String) {
376 self.id = FastBreakV1.nextPlayerId
377 self.playerName = playerName
378 self.gameTokensPlayed = []
379 self.tokensMinted = 0
380
381 FastBreakV1.fastBreakPlayerByID[self.id] = PlayerData(playerName: playerName)
382 }
383
384 /// Play the game of Fast Break with an array of Top Shots
385 /// Each account must own a top shot collection to play fast break
386 ///
387 access(Play) fun play(
388 fastBreakGameID: String,
389 topShots: [UInt64]
390 ): @FastBreakV1.NFT {
391 pre {
392 FastBreakV1.fastBreakGameByID.containsKey(fastBreakGameID): "No such fast break game with gameId: ".concat(fastBreakGameID)
393 }
394
395 /// Update player address mapping
396 if let ownerAddress = self.owner?.address {
397 FastBreakV1.playerAccountMapping[self.id] = ownerAddress
398 FastBreakV1.accountPlayerMapping[ownerAddress] = self.id
399 }
400
401 /// Validate Top Shots
402 let acct = getAccount(self.owner?.address!)
403 let collectionRef = acct.capabilities.borrow<&TopShot.Collection>(/public/MomentCollection)
404 ?? panic("Player does not have top shot collection")
405 let marketV3CollectionRef = acct.capabilities.borrow<&TopShotMarketV3.SaleCollection>(/public/topshotSalev3Collection)
406 let marketV1CollectionRef = acct.capabilities.borrow<&Market.SaleCollection>(/public/topshotSaleCollection)
407
408 /// Must own Top Shots to play Fast Break
409 /// more efficient to borrow ref than to loop
410 ///
411 for flowId in topShots {
412 let topShotRef = collectionRef.borrowMoment(id: flowId)
413 if topShotRef == nil {
414 let hasMarketPlaceV3 = marketV3CollectionRef != nil && marketV3CollectionRef!.borrowMoment(id: flowId) != nil
415 let hasMarketV1 = marketV1CollectionRef != nil && marketV1CollectionRef!.borrowMoment(id: flowId) != nil
416 if !hasMarketPlaceV3 && !hasMarketV1{
417 panic("Top shot not owned in any collection with flowId: ".concat(flowId.toString()))
418 }
419 }
420 }
421
422 let fastBreakGame = FastBreakV1.getFastBreakGame(id: fastBreakGameID)
423 ?? panic("Fast break does not exist with gameId: ".concat(fastBreakGameID))
424
425 /// Cannot mint two tokens for the same Fast Break
426 let existingSubmission = fastBreakGame.getFastBreakSubmissionByPlayerId(playerId: self.id)
427 if existingSubmission != nil {
428 panic("Account already submitted to fast break with playerId: ".concat(self.id.toString()))
429 }
430
431 let fastBreakSubmission = FastBreakV1.FastBreakSubmission(
432 playerId: self.id,
433 fastBreakGameID: fastBreakGameID,
434 topShots: topShots
435 )
436
437 fastBreakGame.submitFastBreak(submission: fastBreakSubmission)
438
439 let fastBreakNFT <- create NFT(
440 fastBreakGameID: fastBreakGameID,
441 serialNumber: self.tokensMinted + 1,
442 topShots: topShots,
443 mintedTo: self.id
444 )
445
446 self.tokensMinted = self.tokensMinted + 1
447 self.gameTokensPlayed.append(fastBreakNFT.id)
448
449 emit FastBreakGameTokenMinted(
450 id: fastBreakNFT.id,
451 fastBreakGameID: fastBreakNFT.fastBreakGameID,
452 serialNumber: fastBreakNFT.serialNumber,
453 mintingDate: fastBreakNFT.mintingDate,
454 topShots: fastBreakNFT.topShots,
455 mintedTo: fastBreakNFT.mintedTo
456 )
457
458 FastBreakV1.totalSupply = FastBreakV1.totalSupply + 1
459 return <- fastBreakNFT
460 }
461
462 /// Update FastBreak Game Submission with an array of Top Shots
463 /// Each account must have a submission before being able to update
464 ///
465 access(Update) fun updateSubmission(
466 fastBreakGameID: String,
467 topShots: [UInt64]
468 ) {
469 pre {
470 FastBreakV1.fastBreakGameByID.containsKey(fastBreakGameID): "No such fast break game with gameId: ".concat(fastBreakGameID)
471 }
472
473 /// Update player address mapping
474 if let ownerAddress = self.owner?.address {
475 FastBreakV1.playerAccountMapping[self.id] = ownerAddress
476 FastBreakV1.accountPlayerMapping[ownerAddress] = self.id
477 }
478
479 /// Validate Top Shots
480 let acct = getAccount(self.owner?.address!)
481 let collectionRef = acct.capabilities.borrow<&TopShot.Collection>(/public/MomentCollection)
482 ?? panic("Player does not have top shot collection")
483 let marketV3CollectionRef = acct.capabilities.borrow<&TopShotMarketV3.SaleCollection>(/public/topshotSalev3Collection)
484 let marketV1CollectionRef = acct.capabilities.borrow<&Market.SaleCollection>(/public/topshotSaleCollection)
485
486 /// Must own Top Shots to play Fast Break
487 /// more efficient to borrow ref than to loop
488 ///
489 for flowId in topShots {
490 let topShotRef = collectionRef.borrowMoment(id: flowId)
491 if topShotRef == nil {
492 let hasMarketPlaceV3 = marketV3CollectionRef != nil && marketV3CollectionRef!.borrowMoment(id: flowId) != nil
493 let hasMarketV1 = marketV1CollectionRef != nil && marketV1CollectionRef!.borrowMoment(id: flowId) != nil
494 if !hasMarketPlaceV3 && !hasMarketV1{
495 panic("Top shot not owned in any collection with flowId: ".concat(flowId.toString()))
496 }
497 }
498 }
499
500 let fastBreakGame = FastBreakV1.getFastBreakGame(id: fastBreakGameID)
501 ?? panic("Fast break does not exist with gameId: ".concat(fastBreakGameID))
502
503 /// Check that the user has a submission for Fast Break game we can update
504 let pastSubmission = fastBreakGame.getFastBreakSubmissionByPlayerId(playerId: self.id)
505 ?? panic("Account already with playerID: ".concat(self.id.toString())
506 .concat(" has not played FastBreak with ID: ".concat(fastBreakGameID)))
507
508 fastBreakGame.updateFastBreakTopshots(playerId: self.id, topshotMoments: topShots)
509
510 // Get the updated submission with new topshot moment Ids
511 let updatedSubmission = fastBreakGame.getFastBreakSubmissionByPlayerId(playerId: self.id)
512 ?? panic("Account already with playerID: ".concat(self.id.toString())
513 .concat(" has not played FastBreak with ID: ".concat(fastBreakGameID)))
514
515 emit FastBreakGameSubmissionUpdated(
516 playerId: self.id,
517 fastBreakGameID: fastBreakGameID,
518 topShots: updatedSubmission.topShots,
519 )
520 }
521
522 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
523 return <- FastBreakV1.createEmptyCollection(nftType: Type<@FastBreakV1.Player>())
524 }
525
526 access(all) view fun getViews(): [Type] {
527 return [
528 Type<MetadataViews.NFTCollectionData>(),
529 Type<MetadataViews.NFTCollectionDisplay>()
530 ]
531 }
532
533 access(all) fun resolveView(_ view: Type): AnyStruct? {
534 switch view {
535 case Type<MetadataViews.NFTCollectionData>():
536 return FastBreakV1.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
537 case Type<MetadataViews.NFTCollectionDisplay>():
538 return FastBreakV1.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
539 }
540 return nil
541 }
542 }
543
544 access(all) struct PlayerData {
545
546 access(all) let id: UInt64
547 access(all) let playerName: String
548
549 init(playerName: String) {
550 self.id = FastBreakV1.nextPlayerId
551 self.playerName = playerName
552 }
553 }
554
555 /// Get a player id by account address
556 ///
557 access(all) view fun getPlayerIdByAccount(accountAddress: Address): UInt64 {
558 return FastBreakV1.accountPlayerMapping[accountAddress]!
559 }
560
561 /// Validate Fast Break Submission topShots
562 ///
563 access(all) view fun validatePlaySubmission(fastBreakGame: FastBreakGame, topShots: [UInt64]): Bool {
564
565 if (topShots.length < 1) {
566 return false
567 }
568
569 if topShots.length > Int(fastBreakGame.numPlayers) {
570 return false
571 }
572
573 return true
574 }
575
576
577 /// The Fast Break game token
578 ///
579 access(all) resource NFT: NonFungibleToken.NFT {
580 access(all) let id: UInt64
581 access(all) let fastBreakGameID: String /// The uuid of the Fast Break Game
582 access(all) let serialNumber: UInt64 /// Each account mints game tokens from 1 => n
583 access(all) let mintingDate: UInt64 /// The block timestamp of the tokens minting
584 access(all) let mintedTo: UInt64 /// The playerId of the minter.
585 access(all) let topShots: [UInt64] /// The top shot ids of the game tokens submission
586
587 access(all) event ResourceDestroyed(
588 id: UInt64 = self.id,
589 serialNumber: UInt64 = self.serialNumber
590 )
591
592 init(
593 fastBreakGameID: String,
594 serialNumber: UInt64,
595 topShots: [UInt64],
596 mintedTo: UInt64,
597 ) {
598 pre {
599 FastBreakV1.fastBreakGameByID.containsKey(fastBreakGameID): "No such fast break with gameId: ".concat(fastBreakGameID)
600 }
601
602 self.id = self.uuid
603 self.fastBreakGameID = fastBreakGameID
604 self.serialNumber = serialNumber
605 self.mintingDate = UInt64(getCurrentBlock().timestamp)
606 self.topShots = topShots
607 self.mintedTo = mintedTo
608 }
609
610 access(all) view fun isWinner(): Bool {
611 if let fastBreak = FastBreakV1.getFastBreakGame(id: self.fastBreakGameID) {
612 if let submission = fastBreak.submissions[self.mintedTo] {
613 return submission.win
614 }
615 }
616 return false
617 }
618
619 access(all) view fun points(): UInt64 {
620 if let fastBreak = FastBreakV1.getFastBreakGame(id: self.fastBreakGameID) {
621 if let submission = fastBreak.submissions[self.mintedTo] {
622 return submission.points
623 }
624 }
625 return 0
626 }
627
628 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
629 return <- FastBreakV1.createEmptyCollection(nftType: Type<@FastBreakV1.NFT>())
630 }
631
632 access(all) view fun getViews(): [Type] {
633 return [
634 Type<MetadataViews.NFTCollectionData>(),
635 Type<MetadataViews.NFTCollectionDisplay>()
636 ]
637 }
638
639 access(all) fun resolveView(_ view: Type): AnyStruct? {
640 switch view {
641 case Type<MetadataViews.NFTCollectionData>():
642 return FastBreakV1.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
643 case Type<MetadataViews.NFTCollectionDisplay>():
644 return FastBreakV1.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
645 }
646 return nil
647 }
648 }
649
650 /// The Fast Break game token collection
651 ///
652 access(all) resource interface FastBreakNFTCollectionPublic : NonFungibleToken.CollectionPublic {
653 access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection})
654 access(all) fun borrowFastBreakNFT(id: UInt64): &FastBreakV1.NFT? {
655 post {
656 (result == nil) || (result?.id == id):
657 "Cannot borrow Fast Break NFT reference: The ID of the returned reference is incorrect"
658 }
659 }
660 }
661
662 /// Capabilities of Fast Break Players
663 ///
664 access(all) resource interface FastBreakPlayer {
665 access(Play) fun play(
666 fastBreakGameID: String,
667 topShots: [UInt64]
668 ): @FastBreakV1.NFT
669 }
670
671 /// Fast Break game collection
672 ///
673 access(all) resource Collection:
674 NonFungibleToken.Collection,
675 FastBreakNFTCollectionPublic
676 {
677
678 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
679
680 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
681 let token <- self.ownedNFTs.remove(key: withdrawID)
682 ?? panic("Could not find a fast break with the given ID in the Fast Break collection. Fast break Id: ".concat(withdrawID.toString()))
683
684 return <-token
685 }
686
687 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
688 let token <- token as! @FastBreakV1.NFT
689 let id: UInt64 = token.id
690
691 let oldToken <- self.ownedNFTs[id] <- token
692
693 destroy oldToken
694 }
695
696 access(all) fun batchDeposit(tokens: @{NonFungibleToken.Collection}) {
697 let keys = tokens.getIDs()
698
699 for key in keys {
700 self.deposit(token: <-tokens.withdraw(withdrawID: key))
701 }
702
703 destroy tokens
704 }
705
706 access(all) view fun getIDs(): [UInt64] {
707 return self.ownedNFTs.keys
708 }
709
710 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
711 return &self.ownedNFTs[id]
712 }
713
714 access(all) view fun borrowFastBreakNFT(id: UInt64): &FastBreakV1.NFT? {
715 return self.borrowNFT(id) as! &FastBreakV1.NFT?
716 }
717
718 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
719 let supportedTypes: {Type: Bool} = {}
720 supportedTypes[Type<@FastBreakV1.NFT>()] = true
721 return supportedTypes
722 }
723
724 // Return whether or not the given type is accepted by the collection
725 // A collection that can accept any type should just return true by default
726 access(all) view fun isSupportedNFTType(type: Type): Bool {
727 if type == Type<@FastBreakV1.NFT>() {
728 return true
729 }
730 return false
731 }
732
733 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
734 return <- FastBreakV1.createEmptyCollection(nftType: Type<@FastBreakV1.NFT>())
735 }
736
737 access(all) view fun getLength(): Int {
738 return self.ownedNFTs.length
739 }
740
741 init() {
742 self.ownedNFTs <- {}
743 }
744 }
745
746 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
747 if nftType != Type<@FastBreakV1.NFT>() {
748 panic("NFT type is not supported")
749 }
750 return <- create Collection()
751 }
752
753 access(all) view fun getContractViews(resourceType: Type?): [Type] {
754 return [Type<MetadataViews.NFTCollectionData>(), Type<MetadataViews.NFTCollectionDisplay>()]
755 }
756
757 access(all) view fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
758 post {
759 result == nil || result!.getType() == viewType: "The returned view must be of the given type or nil"
760 }
761 switch viewType {
762 case Type<MetadataViews.NFTCollectionData>():
763 return MetadataViews.NFTCollectionData(
764 storagePath: /storage/FastBreakGameV1,
765 publicPath: /public/FastBreakGameV1,
766 publicCollection: Type<&FastBreakV1.Collection>(),
767 publicLinkedType: Type<&FastBreakV1.Collection>(),
768 createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
769 return <-FastBreakV1.createEmptyCollection(nftType: Type<@FastBreakV1.NFT>())
770 })
771 )
772 case Type<MetadataViews.NFTCollectionDisplay>():
773 let bannerImage = MetadataViews.Media(
774 file: MetadataViews.HTTPFile(
775 url: "https://nbatopshot.com/static/fastbreak/fast-break-logo.svg"
776 ),
777 mediaType: "image/svg+xml"
778 )
779 let squareImage = MetadataViews.Media(
780 file: MetadataViews.HTTPFile(
781 url: "https://nbatopshot.com/static/fastbreak/fast-break-logo.svg"
782 ),
783 mediaType: "image/png"
784 )
785 return MetadataViews.NFTCollectionDisplay(
786 name: "NBA Top Shot Fast Break",
787 description: "The game of Fast Break is very simple. Collectors will select five players every night for fifteen nights. Each night has different stats and different scores that your team must beat in order to get awarded a win.",
788 externalURL: MetadataViews.ExternalURL("https://nbatopshot.com/fastbreak"),
789 squareImage: squareImage,
790 bannerImage: bannerImage,
791 socials: {
792 "twitter": MetadataViews.ExternalURL("https://twitter.com/nbatopshot"),
793 "discord": MetadataViews.ExternalURL("https://discord.com/invite/nbatopshot"),
794 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nbatopshot")
795 }
796 )
797 }
798 return nil
799 }
800
801 access(all) fun createPlayer(playerName: String): @FastBreakV1.Player {
802 FastBreakV1.nextPlayerId = FastBreakV1.nextPlayerId + UInt64(1)
803
804 emit FastBreakPlayerCreated(
805 id: FastBreakV1.nextPlayerId,
806 playerName: playerName,
807 )
808
809 return <- create FastBreakV1.Player(playerName: playerName)
810 }
811
812 /// Capabilities of the Game Oracle
813 ///
814 access(all) resource interface GameOracle {
815 access(Create) fun createFastBreakRun(id: String, name: String, runStart: UInt64, runEnd: UInt64, fatigueModeOn: Bool)
816 access(Update) fun updateFastBreakRunStatus(id: String, status: UInt8)
817 access(Create) fun createFastBreakGame(
818 id: String,
819 name: String,
820 fastBreakRunID: String,
821 submissionDeadline: UInt64,
822 numPlayers: UInt64
823 )
824 access(Update) fun updateFastBreakGame(id: String, status: UInt8, winner: UInt64)
825 access(Update) fun updateFastBreakScore(fastBreakGameID: String, playerId: UInt64, points: UInt64, win: Bool)
826 access(Update) fun addStatToFastBreakGame(fastBreakGameID: String, name: String, rawType: UInt8, valueNeeded: UInt64)
827 access(Update) fun setSubmissionDeadline(fastBreakGameID: String, deadline: UInt64)
828 }
829
830 /// Fast Break Daemon game oracle implementation
831 ///
832 access(all) resource FastBreakDaemon: GameOracle {
833
834 /// Create a Fast Break Run
835 ///
836 access(Create) fun createFastBreakRun(id: String, name: String, runStart: UInt64, runEnd: UInt64, fatigueModeOn: Bool) {
837 let fastBreakRun = FastBreakV1.FastBreakRun(
838 id: id,
839 name: name,
840 runStart: runStart,
841 runEnd: runEnd,
842 fatigueModeOn: fatigueModeOn
843 )
844 FastBreakV1.fastBreakRunByID[fastBreakRun.id] = fastBreakRun
845 emit FastBreakRunCreated(
846 id: fastBreakRun.id,
847 name: fastBreakRun.name,
848 runStart: fastBreakRun.runStart,
849 runEnd: fastBreakRun.runEnd,
850 fatigueModeOn: fastBreakRun.fatigueModeOn
851 )
852 }
853
854 /// Update the status of a Fast Break Run
855 ///
856 access(Update) fun updateFastBreakRunStatus(id: String, status: UInt8) {
857 let fastBreakRun = FastBreakV1.getFastBreakRun(id: id)
858 let run = fastBreakRun ?? panic("Fast break run does not exist with Id: ".concat(id))
859
860 let runStatus: FastBreakV1.RunStatus = FastBreakV1.RunStatus(rawValue: status)
861 ?? panic("Run status does not exist with rawValue: ".concat(status.toString()))
862
863 run.updateStatus(status: runStatus)
864
865 emit FastBreakRunStatusChange(id: run.id, newRawStatus: run.status.rawValue)
866 }
867
868 /// Create a game of Fast Break
869 ///
870 access(Create) fun createFastBreakGame(
871 id: String,
872 name: String,
873 fastBreakRunID: String,
874 submissionDeadline: UInt64,
875 numPlayers: UInt64
876 ) {
877 let fastBreakGame: FastBreakV1.FastBreakGame = FastBreakV1.FastBreakGame(
878 id: id,
879 name: name,
880 fastBreakRunID: fastBreakRunID,
881 submissionDeadline: submissionDeadline,
882 numPlayers: numPlayers
883 )
884 FastBreakV1.fastBreakGameByID[fastBreakGame.id] = fastBreakGame
885 emit FastBreakGameCreated(
886 id: fastBreakGame.id,
887 name: fastBreakGame.name,
888 fastBreakRunID: fastBreakGame.fastBreakRunID,
889 submissionDeadline: fastBreakGame.submissionDeadline,
890 numPlayers: fastBreakGame.numPlayers
891 )
892 }
893
894 /// Add a Fast Break Statistic to a game of Fast Break during game creation
895 ///
896 access(Update) fun addStatToFastBreakGame(fastBreakGameID: String, name: String, rawType: UInt8, valueNeeded: UInt64) {
897
898 let fastBreakGame = FastBreakV1.getFastBreakGame(id: fastBreakGameID)
899 ?? panic("Fast break does not exist with Id: ".concat(fastBreakGameID))
900
901 let statType: FastBreakV1.StatisticType = FastBreakV1.StatisticType(rawValue: rawType)
902 ?? panic("Fast break stat type does not exist with rawType: ".concat(rawType.toString()))
903
904 let fastBreakStat : FastBreakV1.FastBreakStat = FastBreakV1.FastBreakStat(
905 name: name,
906 type: statType,
907 valueNeeded: valueNeeded
908 )
909
910 fastBreakGame.addStat(stat: fastBreakStat)
911
912 emit FastBreakGameStatAdded(
913 fastBreakGameID: fastBreakGame.id,
914 name: fastBreakStat.name,
915 type: fastBreakStat.type.rawValue,
916 valueNeeded: fastBreakStat.valueNeeded
917 )
918
919 }
920
921 /// Set the submission deadline for a Fast Break
922 ///
923 access(Update) fun setSubmissionDeadline(fastBreakGameID: String, deadline: UInt64) {
924 let fastBreakGame = FastBreakV1.getFastBreakGame(id: fastBreakGameID)
925 ?? panic("Fast break does not exist with Id: ".concat(fastBreakGameID))
926
927 fastBreakGame.setSubmissionDeadline(deadline: deadline)
928 }
929
930 /// Update the status of a Fast Break
931 ///
932 access(Update) fun updateFastBreakGame(id: String, status: UInt8, winner: UInt64) {
933
934 let fastBreakGame = FastBreakV1.getFastBreakGame(id: id)
935 ?? panic("Fast break does not exist with Id: ".concat(id))
936
937 let fastBreakStatus: FastBreakV1.GameStatus = FastBreakV1.GameStatus(rawValue: status)
938 ?? panic("Fast break status does not exist with rawValue: ".concat(status.toString()))
939
940 fastBreakGame.update(status: fastBreakStatus, winner: winner)
941
942 emit FastBreakGameStatusChange(id: fastBreakGame.id, newRawStatus: fastBreakGame.status.rawValue)
943
944 }
945
946 /// Updates the submission scores of a Fast Break
947 ///
948 access(Update) fun updateFastBreakScore(fastBreakGameID: String, playerId: UInt64, points: UInt64, win: Bool) {
949 let fastBreakGame = FastBreakV1.getFastBreakGame(id: fastBreakGameID)
950 ?? panic("Fast break does not exist with Id: ".concat(fastBreakGameID))
951
952 let isNewWin = fastBreakGame.updateScore(playerId: playerId, points: points, win: win)
953
954 if isNewWin {
955 let fastBreakRun = FastBreakV1.getFastBreakRun(id: fastBreakGame.fastBreakRunID)
956 let run = fastBreakRun ?? panic("Could not obtain reference to fast break run with Id: ".concat(fastBreakGame.fastBreakRunID))
957
958 run.incrementRunWinCount(playerId: playerId)
959
960 let submission = fastBreakGame.submissions[playerId]!
961
962 emit FastBreakGameWinner(
963 playerId: playerId,
964 submittedAt: submission.submittedAt,
965 fastBreakGameID: submission.fastBreakGameID,
966 topShots: submission.topShots
967 )
968
969 }
970 }
971 }
972
973 init() {
974 self.CollectionStoragePath = /storage/FastBreakGameV1
975 self.CollectionPublicPath = /public/FastBreakGameV1
976 self.OracleStoragePath = /storage/FastBreakOracleV1
977 self.PlayerStoragePath = /storage/FastBreakPlayerV1
978
979 self.totalSupply = 0
980 self.nextPlayerId = 0
981 self.fastBreakRunByID = {}
982 self.fastBreakGameByID = {}
983 self.fastBreakPlayerByID = {}
984 self.playerAccountMapping = {}
985 self.accountPlayerMapping = {}
986
987 let oracle <- create FastBreakDaemon()
988 self.account.storage.save(<-oracle, to: self.OracleStoragePath)
989
990 }
991}
992