I am looking for help with the data in a football related app I am building.
Take this as a sample of the object I am dealing with:
const squad = {
goalkeepers: [
{ player1: { score: 10 } },
{ player2: { score: 12 } }
],
defenders: [
{ player3: { score: 3 } },
{ player4: { score: 19 } },
{ player5: { score: 5 } },
{ player6: { score: 21 } },
{ player7: { score: 6 } },
],
midfielders: [
{ player8: { score: 7 } },
{ player9: { score: 1 } },
{ player10: { score: 18 } },
{ player11: { score: 11 } },
{ player12: { score: 8 } },
],
attackers: [
{ player13: { score: 7 } },
{ player14: { score: 2 } },
{ player15: { score: 16 } }
]
}
There are 15 players here and I want to divide them into two groups:
- A strongest possible outfield team of 11 players based on their score value.
- The remaining 4 players are to go on the bench.
The twist here is that there is a minimum and maximum number of players required in each position of the 11 outfield players.
- Goalkeepers: EXACTLY 1.
- Defenders: MIN 3, MAX 5.
- Midfielders: MIN 2, MAX 5.
- Forwards: MIN 1, MAX 3.
For anyone familiar with Fantasy Premier League, the rules work the same way:
Your team can play in any formation providing that 1 goalkeeper, at least 3 defenders and at least 1 forward are selected at all times.
I've tried concatinating the arrays to one large array and sorting them by player score value but I can't work out how to calculate the strongest first 11 players from that point while adhering to the position rules.
Any help would be greatly appreciated.
CodePudding user response:
I find a modular approach more intuitive and resilient.
This solution breaks the problem down into first converting your data to a more useful format, then finding all the combination of eleven players, then filtering out those that don't match the rules, then choosing the one with the largest score:
// utility functions
const maximumBy = (fn) => (xs) =>
xs .reduce ((a, x, i) => fn (x) > fn (a) ? x : a, xs [0] || null)
const choose = (n, xs) =>
n < 1 || n > xs .length
? []
: n == 1
? [...xs .map (x => [x])]
: [
...choose (n - 1, xs .slice (1)) .map (ys => [xs [0], ...ys]),
...choose (n , xs .slice (1))
]
// helper functions
const simplify = (squad) =>
Object .entries (squad) .flatMap (
([position, vs]) => vs .flatMap (
(v) => Object.entries (v) .flatMap (([name, s]) => ({position, name, ...s}))
)
)
const validate = (rules) => (lineup) => rules .every (({position, min, max}) => {
const count = lineup .filter (({position: p}) => p == position) .length
return count >= min && count <= max
})
const totalScore = (lineup) =>
lineup .reduce ((t, {score}) => t score, 0)
// main function
const bestLineup = (squad, rules) =>
maximumBy (totalScore) (choose (11, simplify (squad)) .filter (validate (rules)))
// sample data
const rules = [{position: 'goalkeepers', min: 1, max: 1}, {position: 'defenders', min: 3, max: 5}, {position: 'midfielders', min: 2, max: 5}, {position: 'attackers', min: 1, max: 3}]
const squad = {goalkeepers: [{player1: {score: 10}}, {player2: {score: 12}}], defenders: [{player3: {score: 3}}, {player4: {score: 19}}, {player5: {score: 5}}, {player6: {score: 21}}, {player7: {score: 6}}], midfielders: [{player8: {score: 7}}, {player9: {score: 1}}, {player10: {score: 18}}, {player11: {score: 11}}, {player12: {score: 8}}], attackers: [{player13: {score: 7}}, {player14: {score: 2}}, {player15: {score: 16}}]}
// demo
console .log (bestLineup (squad, rules))
.as-console-wrapper {max-height: 100% !important; top: 0}
We start with two general-purpose utility functions we might use in other projects
maximumBy
takes a function that converts a value into a comparable one -- in this problem we use it to extract the score -- and return a function that accepts an array of values, runs that function on each and chooses the one with the largest score. (This version is less efficient than it could be. I'd rather focus on simplicity at the moment.choose
finds all subsets ofn
elements of your array of values. For instance,choose (2, ['a', 'b', 'c', 'd'])
returns[['a', 'b'], ['a', 'c'], ['a', 'd']. ['b', 'c'], ['b', 'd'], ['c', 'd']]
.
Then we have some helper functions,
simplify
turns your initialsquad
format into something more tractable:[ {name: "player1", position: "goalkeepers", score: 10}, {name: "player2", position: "goalkeepers", score: 12}, {name: "player3", position: "defenders", score: 3}, // ... {name: "player15", position: "attackers", score: 16} ]
validate
takes an array of rules such as{position: 'defenders', min: 3, max: 5}
and returns a function that takes a lineup from the squad and reports whether that lineup obeys all the rules.
totalScore
takes a lineup and sums up the scores of all the players
Finally, our main function, bestLineup
accepts a squad and an array of rules, simplifies the squad, chooses all lineups of eleven players, filters it down to just those which validate according to the rules, and chooses the maximum by our function that calculates their total score.
If you want the output in the same format as the input, we can just call one more helper to undo our simplify
; let's call it complexify
:
const complexify = (xs) =>
xs .reduce (
(a, {name, position, score}) => (
(a [position] = a [position] || []), (a [position] .push ({[name]: {score}})), a
), {})
which will transform an array of the simple format used above back into something like this:
{
goalkeepers: [
{player2: {score: 12}}
],
defenders: [
{player4: {score: 19}},
{player5: {score: 5}},
{player6: {score: 21}},
{player7: {score: 6}}
],
midfielders: [
{player8: {score: 7}},
{player10: {score: 18}},
{player11: {score: 11}},
{player12: {score: 8}}
]
attackers: [
{player13: {score: 7}},
{player15: {score: 16}}
],
}
But I would only do this if your unusual data format is forced upon you.