@@ -0,0 +1,233 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math/big"
|
||||
"slices"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/iliadenisov/galaxy/server/internal/model/game"
|
||||
)
|
||||
|
||||
type VoteGroup struct {
|
||||
RaceIndex []int
|
||||
Sum float64
|
||||
}
|
||||
|
||||
type VoteNode struct {
|
||||
ID int
|
||||
Ally bool
|
||||
Next *VoteNode
|
||||
}
|
||||
|
||||
func (n VoteNode) String() string {
|
||||
lh, rh := " ", "."
|
||||
if n.Ally {
|
||||
lh, rh = "{", "}"
|
||||
}
|
||||
return fmt.Sprintf("%s%d%s", lh, n.ID, rh)
|
||||
}
|
||||
|
||||
func (c *Cache) TurnAcceptWinners(v []int) {
|
||||
if c.g.Finished() {
|
||||
panic("game is already has its winner(s)")
|
||||
}
|
||||
if len(v) == 0 {
|
||||
return
|
||||
}
|
||||
for _, ri := range v {
|
||||
c.g.Winner = append(c.g.Winner, c.g.Race[ri].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) TurnCalculateVotes() []int {
|
||||
raceVotes := c.votesByRace()
|
||||
calc := GroupVotes(raceVotes, VotingGraph(c.g.Race, c.RaceIndex))
|
||||
|
||||
c.g.Votes = 0
|
||||
for ri, votes := range raceVotes {
|
||||
v := game.F(votes)
|
||||
c.g.Race[ri].Votes = v
|
||||
c.g.Votes += v
|
||||
}
|
||||
|
||||
return votingWinners(calc, c.g.Votes.F())
|
||||
}
|
||||
|
||||
func VotingGraph(races []game.Race, raceIndex func(uuid.UUID) int) map[int]*VoteNode {
|
||||
nodes := make(map[int]*VoteNode, len(races))
|
||||
for ri := range races {
|
||||
if races[ri].Extinct {
|
||||
continue
|
||||
}
|
||||
r := &races[ri]
|
||||
if _, ok := nodes[ri]; !ok {
|
||||
nodes[ri] = &VoteNode{
|
||||
ID: ri,
|
||||
}
|
||||
}
|
||||
if r.VoteFor != r.ID {
|
||||
vid := raceIndex(r.VoteFor)
|
||||
if !races[vid].Extinct {
|
||||
if _, ok := nodes[vid]; !ok {
|
||||
nodes[vid] = &VoteNode{
|
||||
ID: vid,
|
||||
}
|
||||
}
|
||||
nodes[ri].Next = nodes[vid]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (c *Cache) votesByRace() map[int]float64 {
|
||||
result := make(map[int]float64)
|
||||
for i := range c.g.Map.Planet {
|
||||
p := &c.g.Map.Planet[i]
|
||||
if !p.Owned() {
|
||||
continue
|
||||
}
|
||||
ri := c.RaceIndex(*p.Owner)
|
||||
planetVotes := p.Votes()
|
||||
result[ri] += planetVotes
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GroupVotes(raceVotes map[int]float64, nodes map[int]*VoteNode) []*VoteGroup {
|
||||
votes := maps.Clone(raceVotes)
|
||||
result := make([]*VoteGroup, 0)
|
||||
chains := VotingChains(nodes)
|
||||
chainingRaces := make(map[int]bool)
|
||||
|
||||
for i := range chains {
|
||||
chain := chains[i]
|
||||
if len(chain) == 0 {
|
||||
panic("voters chain is empty")
|
||||
}
|
||||
vg := &VoteGroup{}
|
||||
for j := range chain {
|
||||
node := &chain[j]
|
||||
if node.Ally || j == len(chain)-1 {
|
||||
vg.RaceIndex = append(vg.RaceIndex, node.ID)
|
||||
}
|
||||
vg.Sum += votes[node.ID]
|
||||
votes[node.ID] = 0
|
||||
chainingRaces[node.ID] = true
|
||||
}
|
||||
// find a non-ally group (single race) which already have its votes and merge with a new VoteGroup instead of adding to result
|
||||
if i := slices.IndexFunc(result, func(v *VoteGroup) bool { return len(v.RaceIndex) == 1 && v.RaceIndex[0] == vg.RaceIndex[0] }); i >= 0 && len(vg.RaceIndex) == 1 {
|
||||
result[i].Sum += vg.Sum
|
||||
} else {
|
||||
result = append(result, vg)
|
||||
}
|
||||
}
|
||||
|
||||
for ri, votes := range votes {
|
||||
if _, ok := chainingRaces[ri]; !ok && votes > 0 {
|
||||
result = append(result, &VoteGroup{RaceIndex: []int{ri}, Sum: votes})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func VotingChains(nodes map[int]*VoteNode) [][]VoteNode {
|
||||
visited := make(map[int]bool)
|
||||
result := make([][]VoteNode, 0)
|
||||
raceIds := slices.Collect(maps.Keys(nodes))
|
||||
slices.Sort(raceIds)
|
||||
for _, rid := range raceIds {
|
||||
n := nodes[rid]
|
||||
if v, ok := visited[n.ID]; (ok && v) || n.Next == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
slow, fast := n, n
|
||||
cycled := false
|
||||
var cycleBound *VoteNode
|
||||
for slow != nil && fast != nil && fast.Next != nil {
|
||||
slow = slow.Next
|
||||
fast = fast.Next.Next
|
||||
|
||||
if slow == fast {
|
||||
slow = n
|
||||
|
||||
for slow != fast {
|
||||
slow = slow.Next
|
||||
fast = fast.Next
|
||||
}
|
||||
|
||||
cycled = true
|
||||
cycleBound = slow
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var current *VoteNode
|
||||
if cycled && !visited[slow.ID] {
|
||||
result = append(result, make([]VoteNode, 0))
|
||||
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: slow.ID, Ally: true})
|
||||
visited[slow.ID] = true
|
||||
|
||||
current = slow.Next
|
||||
|
||||
for current != slow {
|
||||
visited[current.ID] = true
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: true})
|
||||
current = current.Next
|
||||
}
|
||||
|
||||
if n == slow {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current = n
|
||||
var finish *VoteNode
|
||||
if cycleBound != nil {
|
||||
if cycleBound == current.Next {
|
||||
finish = current
|
||||
} else {
|
||||
finish = cycleBound
|
||||
}
|
||||
} else {
|
||||
finish = nil
|
||||
}
|
||||
|
||||
if finish != current {
|
||||
result = append(result, make([]VoteNode, 0))
|
||||
}
|
||||
|
||||
for current != finish {
|
||||
visited[current.ID] = current.ID != n.ID && current.Next != nil
|
||||
result[len(result)-1] = append(result[len(result)-1], VoteNode{ID: current.ID, Ally: false})
|
||||
current = current.Next
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func votingWinners(calc []*VoteGroup, sumVotes float64) []int {
|
||||
slices.SortFunc(calc, func(a, b *VoteGroup) int { return cmp.Compare(b.Sum, a.Sum) })
|
||||
|
||||
topVoter := calc[0]
|
||||
maxVotes := &big.Rat{}
|
||||
maxVotes.SetFloat64(topVoter.Sum)
|
||||
|
||||
winVotes := &big.Rat{}
|
||||
winVotes.SetFloat64(sumVotes)
|
||||
winVotes = winVotes.Mul(winVotes, big.NewRat(2, 3))
|
||||
|
||||
if maxVotes.Cmp(winVotes) >= 0 {
|
||||
return topVoter.RaceIndex
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user