Files
galaxy-game/internal/controller/vote.go
T
2026-01-30 12:18:32 +03:00

215 lines
4.5 KiB
Go

package controller
import (
"cmp"
"fmt"
"maps"
"math/big"
"slices"
"github.com/google/uuid"
"github.com/iliadenisov/galaxy/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) TurnCalculateVotes() []int {
raceVotes := c.votesByRace()
calc := GroupVotes(raceVotes, VotingGraph(c.g.Race, c.RaceIndex))
c.g.Votes = 0
for ri, votes := range raceVotes {
c.g.Race[ri].Votes = votes
c.g.Votes += votes
}
return votingWinners(calc, c.g.Votes)
}
func VotingGraph(races []game.Race, raceIndex func(uuid.UUID) int) []*VoteNode {
nodes := make([]*VoteNode, len(races))
for ri := range races {
// TODO: filter inactive (RIP) races
r := &races[ri]
if nodes[ri] == nil {
nodes[ri] = &VoteNode{
ID: ri,
}
}
if r.VoteFor != r.ID {
vid := raceIndex(r.VoteFor)
if nodes[vid] == nil {
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.Owner == uuid.Nil {
continue
}
ri := c.RaceIndex(p.Owner)
planetVotes := p.Votes()
result[ri] += planetVotes
}
return result
}
func GroupVotes(raceVotes map[int]float64, nodes []*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 []*VoteNode) [][]VoteNode {
visited := make(map[int]bool)
result := make([][]VoteNode, 0)
for i := range nodes {
n := nodes[i]
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
}