Smart Contract

FastBreakV1

A.0b2a3299cc857e29.FastBreakV1

Valid From

132,552,377

Deployed

1w ago
Feb 17, 2026, 04:29:37 PM UTC

Dependents

8181796 imports
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