func (r *Replay) detectMeleeObservers(pidBuilds map[byte]int) {
	c := r.Computed
	numObs := 0
	for i, p := range r.Header.Players {
		if p.Type == repcore.PlayerTypeHuman && c.PlayerDescs[i].APM < 25 && pidBuilds[p.ID] < 5 {
			p.Observer = true
			numObs++
		}
	}
	if len(r.Header.Players)-numObs < 2 {
		for _, p := range r.Header.Players {
			p.Observer = false
		}
	}
}
detectMeleeObservers detects observers in Melee games.
package rep
import (
	"math"
	"sort"
	"time"
	"github.com/icza/screp/rep/repcmd"
	"github.com/icza/screp/rep/repcore"
)
type Replay struct {
	Header *Header
	Commands *Commands
	MapData *MapData
	Computed *Computed
}
func (r *Replay) Compute() {
	if r.Computed != nil {
		return
	}
	players := r.Header.Players
	numPlayers := len(players)
	c := &Computed{
		PlayerDescs:    make([]*PlayerDesc, numPlayers),
		PIDPlayerDescs: make(map[byte]*PlayerDesc, numPlayers),
	}
	r.Computed = c
	for i, p := range players {
		pd := &PlayerDesc{
			PlayerID: p.ID,
		}
		c.PlayerDescs[i] = pd
		c.PIDPlayerDescs[p.ID] = pd
	}
	if r.Commands != nil {
		type pidCmdsWrapper struct {
			cmds []repcmd.Cmd
		}
		pidCmdsWrappers := make(map[byte]*pidCmdsWrapper, numPlayers)
		pidBuilds := make(map[byte]int, numPlayers)  
		for _, p := range players {
			pidCmdsWrappers[p.ID] = &pidCmdsWrapper{
				cmds: make([]repcmd.Cmd, 0, len(r.Commands.Cmds)/numPlayers),  
			}
		}
		cmds := r.Commands.Cmds
		for _, cmd := range cmds {
			baseCmd := cmd.BaseCmd()
			if pd := c.PIDPlayerDescs[baseCmd.PlayerID]; pd != nil {
				pd.CmdCount++
				pidCmdsWrapper := pidCmdsWrappers[baseCmd.PlayerID]
				pidCmdsWrapper.cmds = append(pidCmdsWrapper.cmds, cmd)
				baseCmd.IneffKind = CmdIneffKind(pidCmdsWrapper.cmds, len(pidCmdsWrapper.cmds)-1)
				if baseCmd.IneffKind.Effective() {
					pd.EffectiveCmdCount++
				}
			}
			switch x := cmd.(type) {
			case *repcmd.LeaveGameCmd:
				c.LeaveGameCmds = append(c.LeaveGameCmds, x)
			case *repcmd.ChatCmd:
				c.ChatCmds = append(c.ChatCmds, x)
			case *repcmd.BuildCmd:
				pidBuilds[baseCmd.PlayerID]++
			}
		}
		pidPlayerDescs := make(map[byte]*PlayerDesc, numPlayers)
		for pid, pd := range c.PIDPlayerDescs {
			if pd.CmdCount > 0 {
				pidPlayerDescs[pid] = pd
			}
		}
		for i := len(cmds) - 1; i >= 0; i-- {
			cmd := cmds[i]
			baseCmd := cmd.BaseCmd()
			pd := pidPlayerDescs[baseCmd.PlayerID]
			if pd == nil {
				continue
			}
			if baseCmd.Frame > r.Header.Frames || baseCmd.Frame < 0 {
				continue
			}
			pd.LastCmdFrame = baseCmd.Frame
			if len(pidPlayerDescs) == 1 {
				break
			}
			delete(pidPlayerDescs, pd.PlayerID)
		}
		for _, pd := range c.PlayerDescs {
			if pd.LastCmdFrame == 0 {
				continue
			}
			mins := pd.LastCmdFrame.Duration().Minutes()
			pd.APM = int32(float64(pd.CmdCount)/mins + 0.5)
			pd.EAPM = int32(float64(pd.EffectiveCmdCount)/mins + 0.5)
		}
		if r.Header.Type == repcore.GameTypeMelee {
			r.detectMeleeObservers(pidBuilds)
			r.computeMeleeTeams()
		}
		r.computeWinners()
	}
	if r.MapData != nil {
		cx, cy := float64(r.Header.MapWidth*16), float64(r.Header.MapHeight*16)
		sls := r.MapData.StartLocations
		for i, p := range players {
			for j := range sls {
				if p.SlotID == uint16(sls[j].SlotID) {
					pt := &sls[j].Point
					c.PlayerDescs[i].StartLocation = pt
					c.PlayerDescs[i].StartDirection = angleToClock(
						math.Atan2(cy-float64(pt.Y), float64(pt.X)-cx),
					)
					break
				}
			}
		}
	}
}