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) 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 }