Guest User

Untitled

a guest
Jul 3rd, 2024
253
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import {
  2.   BackgammonMessage,
  3.   MessageType,
  4.   Move,
  5.   Place,
  6. } from "./backgammonMessage"
  7.  
  8. import { ExpectedError, isExpectedError } from "./utils"
  9.  
  10. const numericPlace = (p: Place) => {
  11.   if (p === "jail") return 25
  12.   if (p === "free") return 0
  13.   return p
  14. }
  15.  
  16. export type MatchState = {
  17.   going: undefined | 0 | 1
  18.   score: [number, number]
  19.   until: number
  20.  
  21.   boards: [PlayerBoard, PlayerBoard]
  22.   rolls: number[]
  23.  
  24.   jacoby: boolean
  25.   crawford: "notYetActive" | "active" | "disabled"
  26.   doubling: "disabled" | [number, number]
  27.  
  28.   requestingDouble: boolean
  29.  
  30.   winner: undefined | string
  31. }
  32.  
  33. export type BoardInit = [Place, number][]
  34. export const defaultBoard: BoardInit = [
  35.   [6, 5],
  36.   [8, 3],
  37.   [13, 5],
  38.   [24, 2],
  39. ]
  40.  
  41. export class PlayerBoard {
  42.   private points: Map<number, number>
  43.   private jail = 0
  44.   private free = 0
  45.  
  46.   constructor(init: BoardInit, private boardLength = 24) {
  47.     this.points = new Map()
  48.     for (const [i, v] of init) {
  49.       if (i === "jail") {
  50.         this.jail = v
  51.       } else if (i === "free") {
  52.         this.free = v
  53.       } else {
  54.         this.points.set(i, v)
  55.       }
  56.     }
  57.   }
  58.  
  59.   copy() {
  60.     return new PlayerBoard(this.serialize(), this.boardLength)
  61.   }
  62.  
  63.   serialize(): [Place, number][] {
  64.     const points = [...this.points.entries()]
  65.     points.sort(([a], [b]) => a - b)
  66.     return [...points, ["jail", this.jail], ["free", this.free]]
  67.   }
  68.  
  69.   getOur(i: Place) {
  70.     if (i === "free") return this.free
  71.     if (i === "jail") return this.jail
  72.     return this.points.get(i) ?? 0
  73.   }
  74.  
  75.   setOur(i: Place, v: number) {
  76.     this.validatePlace(i)
  77.     if (i === "free") {
  78.       this.free = v
  79.     } else if (i === "jail") {
  80.       this.jail = v
  81.     } else {
  82.       if (v) this.points.set(i, v)
  83.       else this.points.delete(i)
  84.     }
  85.   }
  86.  
  87.   getOther(i: Place) {
  88.     return this.getOur(this.toOtherPlace(i))
  89.   }
  90.  
  91.   setOther(i: Place, v: number) {
  92.     this.setOur(this.toOtherPlace(i), v)
  93.   }
  94.  
  95.   getFurthest(): Place {
  96.     if (this.jail) return "jail"
  97.     let furthest: "free" | number = "free"
  98.  
  99.     for (const occupiedIndex of this.points.keys()) {
  100.       if (furthest === "free" || furthest < occupiedIndex) {
  101.         furthest = occupiedIndex
  102.       }
  103.     }
  104.  
  105.     return furthest
  106.   }
  107.  
  108.   // get list of all potentially possible ways to play the roll, regarding only starting positions
  109.   getOurPotentialMoves(roll: number): Move[] {
  110.     const potential: Move[] = []
  111.     if (this.jail) {
  112.       potential.push(new Move("jail", this.boardLength + 1 - roll))
  113.     } else {
  114.       for (const occupiedIndex of this.points.keys()) {
  115.         const endIndex = occupiedIndex - roll
  116.         if (endIndex >= 1) {
  117.           potential.push(new Move(occupiedIndex, endIndex))
  118.         } else {
  119.           potential.push(new Move(occupiedIndex, "free"))
  120.         }
  121.       }
  122.     }
  123.     return potential
  124.   }
  125.  
  126.   private toOtherPlace = (i: Place) => {
  127.     if (i === "free" || i === "jail") {
  128.       return i
  129.     }
  130.     return this.boardLength - i + 1
  131.   }
  132.  
  133.   private validatePlace(i: Place) {
  134.     if (i === "free" || i === "jail") {
  135.       return
  136.     }
  137.  
  138.     if (i < 1 || i > this.boardLength) throw Error("bad index: " + i)
  139.   }
  140. }
  141.  
  142. export class BackgammonEngine {
  143.   private match: MatchState
  144.  
  145.   constructor(
  146.     config: Partial<MatchState>,
  147.     private players: [string, string],
  148.     private publish: (msg: string) => void,
  149.     initialState: [BoardInit, BoardInit] = [defaultBoard, defaultBoard],
  150.     private resetState: [BoardInit, BoardInit] = [defaultBoard, defaultBoard]
  151.   ) {
  152.     this.publish(
  153.       JSON.stringify({ message: `${this.players[0]} vs ${this.players[1]}` })
  154.     )
  155.  
  156.     const rolls = config.rolls || this.doRoll(false)
  157.  
  158.     this.match = {
  159.       going: rolls[0] > rolls[1] ? 0 : 1,
  160.       score: [0, 0],
  161.       boards: [
  162.         new PlayerBoard(initialState[0]),
  163.         new PlayerBoard(initialState[1]),
  164.       ],
  165.       until: 5,
  166.       rolls,
  167.       crawford: "notYetActive",
  168.       doubling: [0, 0],
  169.       jacoby: true,
  170.       requestingDouble: false,
  171.       winner: undefined,
  172.       ...config,
  173.     }
  174.  
  175.     this.publish(this.getBoardStateMessage())
  176.   }
  177.  
  178.   getBoardStateMessage() {
  179.     const toPlay = this.getOursOther(this.players)[0]
  180.  
  181.     return JSON.stringify({
  182.       match: {
  183.         ...this.match,
  184.         boards: this.match.boards.map((b) => b.serialize()),
  185.       },
  186.       players: this.players,
  187.       toPlay,
  188.     })
  189.   }
  190.  
  191.   private doRoll(allowDoubles = true) {
  192.     const doRoll = () => Math.floor(Math.random() * 6) + 1
  193.     let rolls = []
  194.     do {
  195.       rolls = [doRoll(), doRoll()]
  196.     } while (!allowDoubles && rolls[0] === rolls[1])
  197.     if (rolls[0] === rolls[1]) {
  198.       rolls.push(rolls[0], rolls[0])
  199.     }
  200.  
  201.     this.publish(JSON.stringify({ message: "Rolled " + rolls.join(", ") }))
  202.     return rolls
  203.   }
  204.  
  205.   private getOursOther<T>([a, b]: [T, T]): [T, T] {
  206.     if (this.match.going === 0) {
  207.       return [a, b]
  208.     }
  209.     if (this.match.going === 1) {
  210.       return [b, a]
  211.     }
  212.     throw Error("bad player state")
  213.   }
  214.  
  215.   private advancePlayer() {
  216.     if (this.match.going === undefined) {
  217.       throw Error("no player to advance")
  218.     }
  219.  
  220.     this.match.going = ((this.match.going + 1) % 2) as 0 | 1
  221.   }
  222.  
  223.   private getCubeVal() {
  224.     return this.match.doubling === "disabled"
  225.       ? 1
  226.       : 2 ** Math.max(...this.match.doubling)
  227.   }
  228.  
  229.   private applyRoundWin() {
  230.     if (this.match.going === undefined) {
  231.       throw Error("no player to mark winner")
  232.     }
  233.  
  234.     const [ours, other] = this.getOursOther(this.match.boards)
  235.     let scale = 1
  236.     const cubeVal = this.getCubeVal()
  237.  
  238.     if (ours.getFurthest() === "free") {
  239.       const opponentFurthest = other.getFurthest()
  240.       if (opponentFurthest === "free") {
  241.         throw Error("bad state, both win?")
  242.       }
  243.       if (opponentFurthest === "jail" || opponentFurthest > 12) {
  244.         scale = 3
  245.       } else if (!other.getOther("free")) {
  246.         scale = 2
  247.       }
  248.     }
  249.  
  250.     let gameVal = cubeVal
  251.  
  252.     const winner = this.players[this.match.going]
  253.  
  254.     if (this.match.jacoby && scale > 1 && cubeVal === 1) {
  255.       this.publish(
  256.         JSON.stringify({
  257.           message: `Winner: ${winner}!. 1 point. Mars/Backgammon multiplier disabled due to Jacoby rule.`,
  258.         })
  259.       )
  260.     } else {
  261.       gameVal *= scale
  262.  
  263.       this.publish(
  264.         JSON.stringify({
  265.           message:
  266.             gameVal === 1
  267.               ? `Winner: ${winner}! 1 point.`
  268.               : `Winner: ${winner}! ${gameVal} points.`,
  269.         })
  270.       )
  271.     }
  272.  
  273.     let [ourScore, otherScore] = this.getOursOther(this.match.score)
  274.     ourScore += gameVal
  275.     this.match.score = this.getOursOther([ourScore, otherScore])
  276.     if (ourScore >= this.match.until) {
  277.       this.match.winner = this.players[this.match.going]
  278.     }
  279.  
  280.     this.match.boards = [
  281.       new PlayerBoard(this.resetState[0]),
  282.       new PlayerBoard(this.resetState[1]),
  283.     ]
  284.  
  285.     this.match.rolls = this.doRoll(false)
  286.     this.match.going = this.match.rolls[0] > this.match.rolls[1] ? 0 : 1
  287.  
  288.     if (this.match.doubling !== "disabled") {
  289.       this.match.doubling = [0, 0]
  290.     }
  291.  
  292.     if (
  293.       this.match.crawford === "notYetActive" &&
  294.       ourScore === this.match.until - 1
  295.     ) {
  296.       this.publish(JSON.stringify({ message: "Crawford rule activated!" }))
  297.       this.match.crawford = "active"
  298.     } else if (this.match.crawford === "active") {
  299.       this.publish(
  300.         JSON.stringify({ message: "Crawford rule no longer active!" })
  301.       )
  302.       this.match.crawford = "disabled"
  303.     }
  304.   }
  305.  
  306.   acceptMessage(message: BackgammonMessage): ExpectedError | undefined {
  307.     if (this.match.winner) {
  308.       return ExpectedError("Match over.")
  309.     }
  310.  
  311.     const toPlay = this.getOursOther(this.players)[0]
  312.     if (toPlay !== message.speaker) {
  313.       return ExpectedError("Not your turn")
  314.     }
  315.  
  316.     const isDoubleResponse =
  317.       message.type === MessageType.Accept || message.type === MessageType.Reject
  318.  
  319.     if (this.match.requestingDouble && !isDoubleResponse) {
  320.       return ExpectedError("Must accept or reject double offer.")
  321.     }
  322.     if (!this.match.requestingDouble && isDoubleResponse) {
  323.       return ExpectedError("No doubling was requested.")
  324.     }
  325.  
  326.     switch (message.type) {
  327.       case MessageType.Roll: {
  328.         if (this.match.rolls.length) {
  329.           return ExpectedError("Already rolled.")
  330.         }
  331.         this.match.rolls = this.doRoll()
  332.         break
  333.       }
  334.       case MessageType.Move: {
  335.         if (!message.data?.moves) {
  336.           return { error: "No move list supplied" }
  337.         }
  338.         const result = this.tryApplyMove(message.data.moves)
  339.  
  340.         if ("error" in result) return result
  341.         this.match.boards = result.boards
  342.         this.match.rolls = []
  343.  
  344.         const ourBoard = this.getOursOther(result.boards)[0]
  345.         if (ourBoard.getFurthest() === "free") {
  346.           this.applyRoundWin()
  347.         } else {
  348.           this.advancePlayer()
  349.         }
  350.  
  351.         break
  352.       }
  353.       case MessageType.Double: {
  354.         if (this.match.rolls.length) {
  355.           return ExpectedError("Already rolled. Ask for double at turn start.")
  356.         }
  357.         if (this.match.doubling === "disabled") {
  358.           return ExpectedError("Doubling disabled: match settings.")
  359.         }
  360.         if (this.match.crawford === "active") {
  361.           return ExpectedError("Doubling disabled: Crawford rule.")
  362.         }
  363.         let ourCube = this.getOursOther(this.match.doubling)[0]
  364.         if (ourCube !== 0) {
  365.           return ExpectedError("Doubling disabled: opponent controls cube.")
  366.         }
  367.         this.match.requestingDouble = true
  368.         this.advancePlayer()
  369.         break
  370.       }
  371.       case MessageType.Accept: {
  372.         this.advancePlayer()
  373.         this.match.requestingDouble = false
  374.  
  375.         if (this.match.doubling === "disabled") {
  376.           throw Error("invalid state, doubling disabled")
  377.         }
  378.         let [ourCube, otherCube] = this.getOursOther(this.match.doubling)
  379.         ourCube = otherCube + 1
  380.         otherCube = 0
  381.         this.match.doubling = this.getOursOther([ourCube, otherCube])
  382.         break
  383.       }
  384.       case MessageType.Reject: {
  385.         this.match.requestingDouble = false
  386.         this.match.doubling = [0, 0]
  387.  
  388.         this.advancePlayer()
  389.         this.applyRoundWin()
  390.         break
  391.       }
  392.       default: {
  393.         return ExpectedError("Unknown message type: " + message.type)
  394.       }
  395.     }
  396.  
  397.     this.publish(JSON.stringify({ moveApplied: message }))
  398.     this.publish(this.getBoardStateMessage())
  399.  
  400.     return undefined
  401.   }
  402.  
  403.   tryApplyMove(
  404.     moves: Move[],
  405.     boards: [PlayerBoard, PlayerBoard] = this.match.boards,
  406.     rolls: number[] = this.match.rolls,
  407.     ensureMaximal = true
  408.   ): ExpectedError | { boards: [PlayerBoard, PlayerBoard]; consumed: number } {
  409.     const [_ourBoard, _otherBoard] = this.getOursOther(boards)
  410.  
  411.     const ourBoard = _ourBoard.copy()
  412.     const otherBoard = _otherBoard.copy()
  413.     const rollsToUse = [...rolls]
  414.  
  415.     let consumed = 0
  416.  
  417.     for (const { from, to } of moves) {
  418.       const ourFromCheckers = ourBoard.getOur(from)
  419.       if (ourFromCheckers === 0) {
  420.         return ExpectedError(
  421.           `Invalid move ${from}/${to}: no checkers on ${from}`
  422.         )
  423.       }
  424.  
  425.       if (from === "jail") {
  426.       } else {
  427.         if (ourBoard.getOur("jail")) {
  428.           return ExpectedError(
  429.             `Invalid move ${from}/${to}: must remove pieces from jail`
  430.           )
  431.         }
  432.         const otherFromCheckers = otherBoard.getOther(from)
  433.         if (otherFromCheckers > 0) {
  434.           return ExpectedError(
  435.             `Invalid state ${from}/${to}: opponent has checkers on ${from}`
  436.           )
  437.         }
  438.       }
  439.  
  440.       const delta = numericPlace(from) - numericPlace(to)
  441.       consumed += delta
  442.       let dieIndex = rollsToUse.findIndex((x) => x === delta)
  443.  
  444.       if (to === "free") {
  445.         if (dieIndex === -1) {
  446.           if (ourBoard.getFurthest() !== from) {
  447.             return ExpectedError(
  448.               `Invalid move ${from}/${to}: overshooting only allowed when moving from furthest point (${ourBoard.getFurthest()})`
  449.             )
  450.           }
  451.  
  452.           dieIndex = rollsToUse.findIndex((x) => x >= delta)
  453.           if (dieIndex === -1) {
  454.             return ExpectedError(
  455.               `Invalid move ${from}/${to}: no die available for overshooting (available: ${rollsToUse})`
  456.             )
  457.           }
  458.         }
  459.         const furthest = ourBoard.getFurthest()
  460.         if (furthest === "free" || furthest === "jail" || furthest > 6) {
  461.           return ExpectedError(
  462.             `Invalid move ${from}/${to}: cannot free checkers until all pieces are in home quadrant`
  463.           )
  464.         }
  465.       } else {
  466.         if (dieIndex === -1) {
  467.           return ExpectedError(
  468.             `Invalid move ${from}/${to}: die ${delta} not available (available: ${rollsToUse})`
  469.           )
  470.         }
  471.  
  472.         const otherToCheckers = otherBoard.getOther(to)
  473.         if (otherToCheckers > 1) {
  474.           return ExpectedError(
  475.             `Invalid move ${from}/${to}: opponent has more than one checker on ${to}`
  476.           )
  477.         }
  478.         if (otherToCheckers === 1) {
  479.           otherBoard.setOther(to, 0)
  480.           otherBoard.setOther("jail", otherBoard.getOther("jail") + 1)
  481.         }
  482.       }
  483.  
  484.       rollsToUse.splice(dieIndex, 1)
  485.  
  486.       const ourToCheckers = ourBoard.getOur(to)
  487.       ourBoard.setOur(from, ourFromCheckers - 1)
  488.       ourBoard.setOur(to, ourToCheckers + 1)
  489.     }
  490.  
  491.     const maxConsumption = rolls.reduce((a, b) => a + b, 0)
  492.  
  493.     if (ensureMaximal && consumed < maxConsumption) {
  494.       // first pass
  495.       for (const roll of new Set(rollsToUse)) {
  496.         for (const nextMove of ourBoard.getOurPotentialMoves(roll)) {
  497.           const testMove = [...moves, nextMove]
  498.           if (!("error" in this.tryApplyMove(testMove, boards, rolls, false))) {
  499.             const moveStr = testMove.map((m) => m.toString()).join(" ")
  500.             return ExpectedError(
  501.               `Invalid move: does not maximize travel (see: ${moveStr})`
  502.             )
  503.           }
  504.         }
  505.       }
  506.  
  507.       // backtrack
  508.       const uniqueRolls = new Set(rolls)
  509.       // if all rolls are the same (doubles), first pass is guaranteed to find better path
  510.       if (uniqueRolls.size !== 1) {
  511.         // if not, backtracking is required (grr.)
  512.  
  513.         const dieOrderings = [
  514.           [rolls[0], rolls[1]],
  515.           [rolls[1], rolls[0]],
  516.         ]
  517.  
  518.         for (const [d1, d2] of dieOrderings) {
  519.           const initialOurBoard = _ourBoard.copy()
  520.  
  521.           for (const firstMove of initialOurBoard.getOurPotentialMoves(d1)) {
  522.             const updated = this.tryApplyMove([firstMove], boards, [d1], false)
  523.             if (isExpectedError(updated)) continue
  524.             const { boards: nextBoards, consumed: firstConsume } = updated
  525.             if (firstConsume > consumed) {
  526.               const moveStr = firstMove.toString()
  527.               return ExpectedError(
  528.                 `Invalid move: does not maximize travel (see: ${moveStr})`
  529.               )
  530.             }
  531.             const [nextOurBoard] = this.getOursOther(nextBoards)
  532.  
  533.             for (const secondMove of nextOurBoard.getOurPotentialMoves(d2)) {
  534.               const updated = this.tryApplyMove(
  535.                 [secondMove],
  536.                 nextBoards,
  537.                 [d2],
  538.                 false
  539.               )
  540.  
  541.               if (isExpectedError(updated)) continue
  542.               const { consumed: secondConsume } = updated
  543.               if (firstConsume + secondConsume > consumed) {
  544.                 const moveStr =
  545.                   firstMove.toString() + " " + secondMove.toString()
  546.                 return ExpectedError(
  547.                   `Invalid move: does not maximize travel (see: ${moveStr}, ${consumed} < ${
  548.                     firstConsume + secondConsume
  549.                   })`
  550.                 )
  551.               }
  552.             }
  553.           }
  554.         }
  555.       }
  556.     }
  557.  
  558.     return {
  559.       boards: this.getOursOther([ourBoard, otherBoard]),
  560.       consumed,
  561.     }
  562.   }
  563. }
  564.  
Advertisement
Add Comment
Please, Sign In to add comment