sorare-rewards 0.1.0.beta11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.gitlab-ci.yml +43 -0
- data/.rspec +3 -0
- data/.rubocop.yml +87 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/sorare/rewards/allocation_configuration.yml +165 -0
- data/lib/sorare/rewards/configuration.rb +25 -0
- data/lib/sorare/rewards/interactors/allocations/compute_for_game_week.rb +35 -0
- data/lib/sorare/rewards/interactors/allocations/compute_for_league.rb +40 -0
- data/lib/sorare/rewards/interactors/allocations/compute_for_quality.rb +96 -0
- data/lib/sorare/rewards/interactors/allocations/compute_for_rarity.rb +34 -0
- data/lib/sorare/rewards/interactors/build.rb +33 -0
- data/lib/sorare/rewards/interactors/cards/pick_for_division.rb +44 -0
- data/lib/sorare/rewards/interactors/cards/pick_for_division_and_rarity.rb +37 -0
- data/lib/sorare/rewards/interactors/cards/pick_for_division_rarity_and_quality.rb +49 -0
- data/lib/sorare/rewards/interactors/cards/pick_for_game_week.rb +40 -0
- data/lib/sorare/rewards/interactors/cards/pick_for_league.rb +64 -0
- data/lib/sorare/rewards/interactors/pick.rb +34 -0
- data/lib/sorare/rewards/interactors/supply/compute_for_game_week.rb +28 -0
- data/lib/sorare/rewards/interactors/supply/compute_for_league.rb +24 -0
- data/lib/sorare/rewards/interactors/supply/compute_for_quality.rb +37 -0
- data/lib/sorare/rewards/interactors/supply/compute_for_rarity.rb +73 -0
- data/lib/sorare/rewards/interactors/tiers/qualify_players.rb +69 -0
- data/lib/sorare/rewards/interactors/tiers/qualify_supply.rb +40 -0
- data/lib/sorare/rewards/random.rb +14 -0
- data/lib/sorare/rewards/transposer.rb +28 -0
- data/lib/sorare/rewards/version.rb +7 -0
- data/lib/sorare/rewards.rb +38 -0
- data/sorare-rewards.gemspec +44 -0
- metadata +220 -0
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'interactor'
|
5
|
+
|
6
|
+
module Sorare
|
7
|
+
module Rewards
|
8
|
+
module Allocations
|
9
|
+
# ComputeForQuality computes the reward allocations for a given quality supply between the divisions of a league
|
10
|
+
class ComputeForQuality
|
11
|
+
include Interactor
|
12
|
+
|
13
|
+
delegate :supply, :config, :randomizer, :allocated, to: :context
|
14
|
+
|
15
|
+
def call
|
16
|
+
check_config!
|
17
|
+
|
18
|
+
context.allocated = Array.new(config.length, 0)
|
19
|
+
allocate!
|
20
|
+
context.quality_allocations = allocations
|
21
|
+
end
|
22
|
+
|
23
|
+
def allocations
|
24
|
+
config.length.times.map do |division|
|
25
|
+
[Sorare::Rewards.configuration.transform_division.call(division + 1), allocated[division]]
|
26
|
+
end.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def allocate!
|
30
|
+
allocate_cards
|
31
|
+
allocate_pct if has?('pct')
|
32
|
+
allocate_extra if has?('pct')
|
33
|
+
allocate_loop if has?('loop')
|
34
|
+
end
|
35
|
+
|
36
|
+
def allocate_divisions(&block)
|
37
|
+
config.each_with_index(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
def allocate_cards
|
41
|
+
allocate_divisions do |division_config, division|
|
42
|
+
count = [remaining_supply, division_config['cards'] || 0].min
|
43
|
+
|
44
|
+
allocated[division] += count
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def allocate_pct
|
49
|
+
allocate_divisions do |division_config, division|
|
50
|
+
count = [remaining_supply, (supply * (division_config['pct'] || 0)).floor].min
|
51
|
+
|
52
|
+
allocated[division] += count
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def allocate_loop
|
57
|
+
while remaining_supply.positive?
|
58
|
+
allocate_divisions do |division_config, division|
|
59
|
+
allocated[division] += [division_config['loop'] || 0, remaining_supply].min
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def allocate_extra
|
65
|
+
allocate_divisions do |division_config, division|
|
66
|
+
count = pick_extra_supply((supply * (division_config['pct'] || 0)).modulo(1))
|
67
|
+
|
68
|
+
allocated[division] += count
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def remaining_supply
|
73
|
+
supply - allocated.sum
|
74
|
+
end
|
75
|
+
|
76
|
+
def pick_extra_supply(probability)
|
77
|
+
reward_probability = randomizer.rand
|
78
|
+
return 0 unless remaining_supply.positive?
|
79
|
+
return 0 unless reward_probability < probability
|
80
|
+
|
81
|
+
1
|
82
|
+
end
|
83
|
+
|
84
|
+
def has?(key)
|
85
|
+
!config.first[key].nil?
|
86
|
+
end
|
87
|
+
|
88
|
+
def check_config!
|
89
|
+
return if config && [0, 1].include?(config.sum { |q| q['pct'] || 0 })
|
90
|
+
|
91
|
+
context.fail!(error: 'Invalid config')
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Allocations
|
10
|
+
# ComputeForRarity computes the reward allocations for a rarity in a league
|
11
|
+
class ComputeForRarity
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :supply, :config, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.fail!(error: 'Invalid config') unless config
|
18
|
+
|
19
|
+
context.rarity_allocations = supply.each_with_index.map do |tier_supply, tier|
|
20
|
+
ComputeForQuality.call!(
|
21
|
+
**context.to_h,
|
22
|
+
supply: tier_supply,
|
23
|
+
config: tier_config(tier)
|
24
|
+
).quality_allocations
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def tier_config(tier)
|
29
|
+
config[Sorare::Rewards.configuration.transform_tier.call(tier)]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'interactor'
|
7
|
+
|
8
|
+
module Sorare
|
9
|
+
module Rewards
|
10
|
+
# Build builds the rewards structure for a given game week
|
11
|
+
class Build
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :data, :salt, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.game_week_supply = game_week_supply
|
18
|
+
context.allocations = allocate!
|
19
|
+
end
|
20
|
+
|
21
|
+
def allocate!
|
22
|
+
Allocations::ComputeForGameWeek.call!(
|
23
|
+
**data.to_h, supply: game_week_supply, salt: salt
|
24
|
+
).game_week_allocations
|
25
|
+
end
|
26
|
+
|
27
|
+
def game_week_supply
|
28
|
+
@game_week_supply ||= Supply::ComputeForGameWeek.call!(**data.to_h, salt: salt)
|
29
|
+
.game_week_supply
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'interactor'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Sorare
|
9
|
+
module Rewards
|
10
|
+
module Cards
|
11
|
+
# PickForDivision picks the rewards for a given division
|
12
|
+
# Receive a list of eligible qualified cards
|
13
|
+
# {
|
14
|
+
# 'rare' => {
|
15
|
+
# 0 => ['kylian-mbappe-lottin', 'kylian-mbappe-lottin', 'keylor-navas', ...]
|
16
|
+
# }
|
17
|
+
# }
|
18
|
+
# A game week league supply
|
19
|
+
# {
|
20
|
+
# 'rare' => { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...},
|
21
|
+
# 'super_rare' => { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...}
|
22
|
+
# }
|
23
|
+
# And a reward allocations
|
24
|
+
# {
|
25
|
+
# 'rare' => [5, 15, 0, 0],
|
26
|
+
# 'super_rare' => [1, 4, 0, 0],
|
27
|
+
# 'unique' => [0, 0, 1, 0]
|
28
|
+
# }
|
29
|
+
class PickForDivision
|
30
|
+
include Interactor
|
31
|
+
|
32
|
+
delegate :allocations, :cards, :qualified_players, :supply, to: :context
|
33
|
+
|
34
|
+
def call
|
35
|
+
context.division_picks = allocations.keys.index_with do |rarity|
|
36
|
+
PickForDivisionAndRarity.call!(
|
37
|
+
**context.to_h, cards: cards[rarity], allocations: allocations[rarity], supply: supply[rarity]
|
38
|
+
).rarity_picks
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Cards
|
10
|
+
# PickForDivisionAndRarity picks the rewards for a given division and rarity
|
11
|
+
# Receive a randomizer, a list of eligible qualified cards of the corresponding rarity
|
12
|
+
# {
|
13
|
+
# 'tier_0' => ['kylian-mbappe-lottin', 'kylian-mbappe-lottin', 'keylor-navas', ...]
|
14
|
+
# }
|
15
|
+
# A game week league rarity supply
|
16
|
+
# { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...}
|
17
|
+
# And a reward allocations
|
18
|
+
# { 'tier_0' => 1, 'tier_1' => 2 }
|
19
|
+
class PickForDivisionAndRarity
|
20
|
+
include Interactor
|
21
|
+
|
22
|
+
delegate :allocations, :cards, to: :context
|
23
|
+
|
24
|
+
def call
|
25
|
+
context.rarity_picks = supply!
|
26
|
+
end
|
27
|
+
|
28
|
+
def supply!
|
29
|
+
allocations.keys.index_with do |tier|
|
30
|
+
PickForDivisionRarityAndQuality.call!(**context.to_h, allocations: allocations[tier],
|
31
|
+
cards: cards[tier]).picks
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'interactor'
|
5
|
+
|
6
|
+
module Sorare
|
7
|
+
module Rewards
|
8
|
+
module Cards
|
9
|
+
# PickForDivisionRarityAndQuality picks the rewards for a given division, rarity, and quality
|
10
|
+
# Receive a list of eligible qualified cards
|
11
|
+
# ['kylian-mbappe-lottin', 'kylian-mbappe-lottin', 'keylor-navas', ...]
|
12
|
+
# A game week league rarity supply
|
13
|
+
# { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...}
|
14
|
+
# And a reward allocations
|
15
|
+
# 15
|
16
|
+
class PickForDivisionRarityAndQuality
|
17
|
+
include Interactor
|
18
|
+
|
19
|
+
MISSING_CARDS = 'There are not enough cards to fulfill the requirements'
|
20
|
+
|
21
|
+
delegate :cards, :allocations, :supply, :force, :picks, :randomizer, to: :context
|
22
|
+
|
23
|
+
def call
|
24
|
+
context.picks = []
|
25
|
+
|
26
|
+
pick!
|
27
|
+
check_length! unless force
|
28
|
+
reorder!
|
29
|
+
end
|
30
|
+
|
31
|
+
def pick!
|
32
|
+
while picks.length != allocations && cards.length.positive?
|
33
|
+
picks.push(cards.delete_at(randomizer.rand * cards.length))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def reorder!
|
38
|
+
picks.sort! { |a, b| supply[a]['rank'].to_i <=> supply[b]['rank'].to_i }
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_length!
|
42
|
+
return if picks.length == allocations
|
43
|
+
|
44
|
+
context.fail!(error: MISSING_CARDS)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'interactor'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Sorare
|
9
|
+
module Rewards
|
10
|
+
module Cards
|
11
|
+
# PickForGameWeek picks the rewards for a given game week
|
12
|
+
# Receive a Game Week reward supply data
|
13
|
+
# { public_seed: 123, playing_players: [...], supply: {...} }
|
14
|
+
# And a reward allocations
|
15
|
+
# {
|
16
|
+
# 'global-all_star' => {
|
17
|
+
# 'D1' => {
|
18
|
+
# 'rare' => { 'tier_0': 1, 'tier_1': 1 }
|
19
|
+
# }
|
20
|
+
# }
|
21
|
+
# }
|
22
|
+
class PickForGameWeek
|
23
|
+
include Interactor
|
24
|
+
|
25
|
+
delegate :supply, :allocations, :randomizer, :public_seed, :salt, to: :context
|
26
|
+
|
27
|
+
def call
|
28
|
+
context.randomizer = Sorare::Rewards::Random.new(public_seed, salt)
|
29
|
+
context.picks = picks
|
30
|
+
end
|
31
|
+
|
32
|
+
def picks
|
33
|
+
allocations.keys.index_with do |league|
|
34
|
+
PickForLeague.call!(**context.to_h, supply: supply[league], allocations: allocations[league]).league_picks
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'active_support/core_ext/enumerable'
|
5
|
+
require 'interactor'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module Sorare
|
9
|
+
module Rewards
|
10
|
+
module Cards
|
11
|
+
# PickForLeague picks the rewards for a given league
|
12
|
+
# Receive a game week league supply
|
13
|
+
# {
|
14
|
+
# 'rare' => { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...},
|
15
|
+
# 'super_rare' => { 'kylian-mbappe-lottin' => { 'rank' => 1 }, ...}
|
16
|
+
# }
|
17
|
+
# And a reward allocations
|
18
|
+
# {
|
19
|
+
# 'D1' => {
|
20
|
+
# 'rare' => { 'tier_0' => 1, 'tier_1' => 2 }
|
21
|
+
# }
|
22
|
+
# }
|
23
|
+
class PickForLeague
|
24
|
+
include Interactor
|
25
|
+
|
26
|
+
delegate :supply, :allocations, :cards, to: :context
|
27
|
+
|
28
|
+
def call
|
29
|
+
context.cards = {}
|
30
|
+
add_cards!
|
31
|
+
|
32
|
+
context.league_picks = allocations.keys.index_with do |division|
|
33
|
+
PickForDivision.call!(
|
34
|
+
**context.to_h, cards: cards, allocations: allocations[division]
|
35
|
+
).division_picks
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_cards!
|
40
|
+
supply.keys.index_with do |rarity|
|
41
|
+
cards[rarity] = {}
|
42
|
+
qualify_and_add!(rarity)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def qualify_and_add!(rarity)
|
47
|
+
qualified_players = Tiers::QualifyPlayers.call!(sorted_supply: supply[rarity]).players
|
48
|
+
qualified_players.each_with_index.map do |players, tier|
|
49
|
+
transformed_tier = Sorare::Rewards.configuration.transform_tier.call(tier)
|
50
|
+
cards[rarity][transformed_tier] = []
|
51
|
+
|
52
|
+
players.each do |player_slug|
|
53
|
+
add(player_slug, rarity, transformed_tier)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def add(player_slug, rarity, tier)
|
59
|
+
cards[rarity][tier] += Array.new(supply[rarity][player_slug]['supply'], player_slug)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
require 'active_support/core_ext/module/delegation'
|
6
|
+
require 'interactor'
|
7
|
+
|
8
|
+
module Sorare
|
9
|
+
module Rewards
|
10
|
+
# Pick the rewards for a given game week
|
11
|
+
# Receive a Game Week reward supply data
|
12
|
+
# And a reward allocations
|
13
|
+
# {
|
14
|
+
# 'global-all_star' => {
|
15
|
+
# 'rare' => {
|
16
|
+
# 'D1' => { 'tier_0' => 0, 'tier_1' => 1 }
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
class Pick
|
21
|
+
include Interactor
|
22
|
+
|
23
|
+
delegate :allocations, :data, :salt, to: :context
|
24
|
+
|
25
|
+
# Depending on the way the allocations interpreter is implemented it could be done by him or we
|
26
|
+
# update the allocations process to match the structure
|
27
|
+
def call
|
28
|
+
context.picks = Sorare::Rewards::Cards::PickForGameWeek.call!(
|
29
|
+
**data.to_h, salt: salt, allocations: Transposer.transpose_allocations(allocations)
|
30
|
+
).picks
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Supply
|
10
|
+
# ComputeForGameWeek computes the rewardable supply of a game week based on the overall supply
|
11
|
+
class ComputeForGameWeek
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :public_seed, :salt, :supply, :playing_players, :game_week_supply, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.game_week_supply = supply.keys.index_with do |league|
|
18
|
+
ComputeForLeague.call!(**context.to_h, randomizer: randomizer, supply: supply[league]).league_supply
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def randomizer
|
23
|
+
@randomizer ||= Sorare::Rewards::Random.new(public_seed, salt)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Supply
|
10
|
+
# ComputeForLeague computes the rewardable supply of a league
|
11
|
+
class ComputeForLeague
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :randomizer, :supply, :playing_players, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.league_supply = supply.keys.index_with do |rarity|
|
18
|
+
ComputeForRarity.call!(**context.to_h, supply: supply[rarity]).rarity_supply
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Supply
|
10
|
+
# ComputeForQuality computes the rewardable supply for a given quality between the divisions of a league
|
11
|
+
class ComputeForQuality
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :randomizer, :total_supply, :tier_supply, :rewarded, :rewardable, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.quality_supply = total_supply.zero? ? 0 : quality_supply
|
18
|
+
end
|
19
|
+
|
20
|
+
def quality_supply
|
21
|
+
plain, extra = (tier_supply.to_f * rewardable).divmod(total_supply)
|
22
|
+
|
23
|
+
plain + remaining_supply(plain, extra)
|
24
|
+
end
|
25
|
+
|
26
|
+
def remaining_supply(plain, extra)
|
27
|
+
reward_probability = randomizer.rand
|
28
|
+
return 0 if rewarded.sum + plain >= rewardable
|
29
|
+
return 0 if extra.zero?
|
30
|
+
return 0 if reward_probability > (extra.to_f / total_supply)
|
31
|
+
|
32
|
+
1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/enumerable'
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
5
|
+
require 'interactor'
|
6
|
+
|
7
|
+
module Sorare
|
8
|
+
module Rewards
|
9
|
+
module Supply
|
10
|
+
# ComputeForRarity computes the rewardable supply for a given rarity
|
11
|
+
class ComputeForRarity
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :randomizer, :supply, :playing_players, :rewardable, :rarity_supply,
|
15
|
+
:minimum_remaining_games, to: :context
|
16
|
+
|
17
|
+
def call
|
18
|
+
context.rarity_supply = []
|
19
|
+
|
20
|
+
context.rewardable = compute_rewardable_supply!
|
21
|
+
distribute_in_tiers!
|
22
|
+
end
|
23
|
+
|
24
|
+
def compute_rewardable_supply!
|
25
|
+
total_supply = playing_players.keys.sum do |player|
|
26
|
+
player_supply(player)
|
27
|
+
end
|
28
|
+
|
29
|
+
rounded_supply(total_supply)
|
30
|
+
end
|
31
|
+
|
32
|
+
def distribute_in_tiers!
|
33
|
+
ctx = Tiers::QualifySupply.call!(sorted_supply: supply)
|
34
|
+
ctx.supply.each do |tier_supply|
|
35
|
+
rarity_supply.push(
|
36
|
+
ComputeForQuality.call!(
|
37
|
+
randomizer: randomizer,
|
38
|
+
total_supply: ctx.count,
|
39
|
+
tier_supply: tier_supply,
|
40
|
+
rewardable: rewardable,
|
41
|
+
rewarded: rarity_supply
|
42
|
+
).quality_supply
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def player_supply(slug)
|
48
|
+
remaining_games = playing_players[slug] || 0
|
49
|
+
return 0 unless remaining_games.positive?
|
50
|
+
return 0 unless supply[slug]
|
51
|
+
|
52
|
+
supply[slug]['supply'].to_f / corrected_remaining_games(remaining_games)
|
53
|
+
end
|
54
|
+
|
55
|
+
def rounded_supply(float_supply)
|
56
|
+
float_supply.floor + remaining_supply(float_supply.modulo(1))
|
57
|
+
end
|
58
|
+
|
59
|
+
# To be improved to handle properly seasons overlap
|
60
|
+
def corrected_remaining_games(remaining_games)
|
61
|
+
[remaining_games, minimum_remaining_games || 10].max
|
62
|
+
end
|
63
|
+
|
64
|
+
def remaining_supply(reward_probability)
|
65
|
+
probability = randomizer.rand
|
66
|
+
return 0 unless probability < reward_probability
|
67
|
+
|
68
|
+
1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/core_ext/module/delegation'
|
4
|
+
require 'interactor'
|
5
|
+
|
6
|
+
module Sorare
|
7
|
+
module Rewards
|
8
|
+
module Tiers
|
9
|
+
# QualifyPlayers qualifies a list of sorted supply in tiers based on its rank or a provided tier
|
10
|
+
# Returns an array of tiers (array) containing the list of slugs for that tier
|
11
|
+
class QualifyPlayers
|
12
|
+
include Interactor
|
13
|
+
|
14
|
+
delegate :sorted_supply, to: :context
|
15
|
+
|
16
|
+
def call
|
17
|
+
context.players = by_rank ? qualified_players_by_rank : qualified_players_by_tier
|
18
|
+
end
|
19
|
+
|
20
|
+
# Use the tier specified within the data
|
21
|
+
def qualified_players_by_tier
|
22
|
+
tiers = Sorare::Rewards.configuration.tiers.times.map { |_| [] }
|
23
|
+
sorted_supply.each do |slug, data|
|
24
|
+
tiers[data['tier']] << slug
|
25
|
+
end
|
26
|
+
|
27
|
+
tiers
|
28
|
+
end
|
29
|
+
|
30
|
+
# Use the rank in the array
|
31
|
+
def qualified_players_by_rank
|
32
|
+
nb_of_tiers.times.map do |tier|
|
33
|
+
sorted_supply.keys[tier_index(tier)..tier_index(tier + 1) - 1]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def tier_depth
|
38
|
+
@tier_depth ||= sorted_supply.keys.length / (nb_of_tiers**2 - 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
def nb_of_tiers
|
42
|
+
@nb_of_tiers ||= begin
|
43
|
+
default = Sorare::Rewards.configuration.tiers
|
44
|
+
default -= 1 while (default**2 - 1) > sorted_supply.keys.length
|
45
|
+
|
46
|
+
default
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def tier_index(tier)
|
51
|
+
return 0 unless tier.positive?
|
52
|
+
return sorted_supply.keys.length if tier == nb_of_tiers
|
53
|
+
|
54
|
+
tier_index(tier - 1) + tier_depth * tier_size(tier - 1)
|
55
|
+
end
|
56
|
+
|
57
|
+
def tier_size(tier)
|
58
|
+
return 1 unless tier.positive?
|
59
|
+
|
60
|
+
tier_size(tier - 1) * 2
|
61
|
+
end
|
62
|
+
|
63
|
+
def by_rank
|
64
|
+
@by_rank ||= sorted_supply.values.first&.dig('tier').nil?
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|