Find Chess960 starting position identifier

Revision as of 21:57, 23 July 2022 by Markjreed (talk | contribs) (→‎{{header|Common Lisp}}: Add implementation.)

As described on the Chess960 page, Chess960 (a.k.a Fischer Random Chess, Chess9LX) is a variant of chess where the array of pieces behind the pawns is randomized at the start of the game to minimize the value of opening theory "book knowledge". That task is to generate legal starting positions, and some of the solutions accept a standard Starting Position Identifier number ("SP-ID"), and generate the corresponding position.

Task
Find Chess960 starting position identifier
You are encouraged to solve this task according to the task description, using any language you may know.
Task

This task is to go the other way: given a starting array of pieces (provided in any form that suits your implementation, whether string or list or array, of letters or Unicode chess symbols or enum values, etc.), derive its unique SP-ID. For example, given the starting array QNRBBNKR (or ♕♘♖♗♗♘♔♖ or ♛♞♜♝♝♞♚♜), which we assume is given as seen from White's side of the board from left to right, your (sub)program should return 105; given the starting lineup of standard chess, it should return 518.

You may assume the input is a valid Chess960 position; detecting invalid input (including illegal characters or starting arrays with the bishops on the same color square or the king not between the two rooks) is optional.

Algorithm

The derivation is the inverse of the algorithm given at Wikipedia, and goes like this (we'll use the standard chess setup as an example).

1. Ignoring the Queen and Bishops, find the positions of the Knights within the remaining five spaces (in the standard array they're in the second and fourth positions), and then find the index number of that combination. There's a table at the above Wikipedia article, but it's just the possible positions sorted left to right and numbered 0 to 9: 0=NN---, 1=N-N--, 2=N--N-, 3=N---N, 4=-NN--, etc; our pair is combination number 5. Call this number N. N=5

2. Still ignoring the Bishops, find the position of the Queen in the remaining 6 spaces; number them 0..5 from left to right and call the index of the Queen's position Q. In our example, Q=2.

3. Finally, find the positions of the two bishops within their respective sets of four like-colored squares. It's important to note here that the board in chess is placed such that the leftmost position on the home row is on a dark square and the rightmost a light. So if we number the squares of each color 0..3 from left to right, the dark bishop in the standard position is on square 1 (D=1), and the light bishop is on square 2 (L=2).

4. Then the position number is given by 4(4(6N + Q)+D)+L, which reduces to 96N + 16Q + 4D + L. In our example, that's 96×5 + 16×2 + 4×1 + 2 = 480 + 32 + 4 + 2 = 518.

Note that an earlier iteration of this page contained an incorrect description of the algorithm which would give the same SP-ID for both of the following two positions.

   RQNBBKRN = 601
   RNQBBKRN = 617

BASIC

Commodore BASIC

Works with: Commodore BASIC version 2.0

Unlike the solution for the reverse task, which uses DO/LOOP and so requires at least Commodore BASIC 3.5, this should work on any version.

<lang basic>100 REM DERIVE SP-ID FROM CHESS960 POS 110 READ A$: IF A$="" THEN END 120 PRINT A$":"; 130 GOSUB 170 140 PRINT SP 150 GOTO 110 160 DATA QNRBBNKR, RNBQKBNR, RQNBBKRN, RNQBBKRN, 170 IF LEN(A$)=8 THEN 190 180 PRINT "ARRAY MUST BE 8 PIECES.": SP=-1: RETURN 190 K=0:Q=0:B=0:N=0:R=0 200 FOR I=0 TO 7 210 : K(I)=0:Q(I)=0:B(I)=0:N(I)=0:R(I)=0 220 NEXT I 230 FOR I=1 TO 8 240 : P$=MID$(A$,I,1) 250 : IF P$="Q" THEN Q(Q)=I: Q=Q+1: GOTO 310 260 : IF P$="K" THEN K(K)=I: K=K+1: GOTO 310 270 : IF P$="B" THEN B(B)=I: B=B+1: GOTO 310 280 : IF P$="N" THEN N(N)=I: N=N+1: GOTO 310 290 : IF P$="R" THEN R(R)=I: R=R+1: GOTO 310 300 : PRINT "ILLEGAL PIECE '"P$"'.": SP=-1: RETURN 310 NEXT I 320 IF K<>1 THEN PRINT "THERE MUST BE EXACTLY ONE KING.": SP=-1: RETURN 330 IF Q<>1 THEN PRINT "THERE MUST BE EXACTLY ONE QUEEN.": SP=-1: RETURN 340 IF B<>2 THEN PRINT "THERE MUST BE EXACTLY TWO BISHOPS.": SP=-1: RETURN 350 IF N<>2 THEN PRINT "THERE MUST BE EXACTLY TWO KNIGHTS.": SP=-1: RETURN 360 IF R<>2 THEN PRINT "THERE MUST BE EXACTLY TWO ROOKS.": SP=-1: RETURN 370 IF (K(0) > R(0)) AND (K(0) < R(1)) THEN 390 380 PRINT "KING MUST BE BETWEEN THE ROOKS.": SP=-1: RETURN 390 IF (B(0) AND 1) <> (B(1) AND 1) THEN 410 400 PRINT "BISHOPS MUST BE ON OPPOSITE COLORS.": SP=-1: RETURN 410 FOR I=0 TO 1 420 : N=N(I) 430 : IF N(I)>Q(I) THEN N=N-1 440 : FOR J=0 TO 1 450 : IF N(I)>B(J) THEN N=N-1 460 : NEXT J 470 : N(I)=N 480 NEXT I 490 N0=1: N1=2 500 FOR N=0 TO 9 510 : IF N0=N(0) AND N1=N(1) THEN 550 520 : N1=N1+1 530 : IF N1>5 THEN N0=N0+1: N1=N0+1 540 NEXT N 550 Q=Q(0)-1 560 FOR I=0 TO 1 570 : IF Q(0)>B(I) THEN Q=Q-1 580 NEXT I 590 FOR I=0 TO 1 600 : B=B(I)-1 610 : IF B AND 1 THEN L=INT(B/2) 620 : IF (B AND 1)=0 THEN D=B/2 630 NEXT I 640 SP = 96*N+16*Q+4*D+L 650 RETURN</lang>

Output:
READY.
RUN
QNRBBNKR: 105
RNBQKBNR: 518
RQNBBKRN: 601
RNQBBKRN: 617

READY.

FreeBASIC

<lang freebasic>Sub SP_ID(PosicPiezas As String)

   Dim As String pieza
   Dim As Integer pQ(), pK(), pB(), pN(), pR(), i, j
   Dim As Integer Q, K, B, N, R, L, D
   
   For i = 1 To 8
       pieza = Mid(PosicPiezas, i, 1)
       Select Case pieza
       Case "Q"
           Redim Preserve pQ(Q) : pQ(Q) = i: Q += 1
       Case "K"
           Redim Preserve pK(K) : pK(K) = i: K += 1
       Case "B"
           Redim Preserve pB(B) : pB(B) = i: B += 1
       Case "N"
           Redim Preserve pN(N) : pN(N) = i: N += 1
       Case "R"
           Redim Preserve pR(R) : pR(R) = i: R += 1
       Case Else 
           Print "ILLEGAL PIECE '"; pieza; "'.": Exit Sub
       End Select
   Next i
   
   If K <> 1 Then Print "THERE MUST BE EXACTLY ONE KING."
   If Q <> 1 Then Print "THERE MUST BE EXACTLY ONE QUEEN."
   If B <> 2 Then Print "THERE MUST BE EXACTLY TWO BISHOPS."
   If N <> 2 Then Print "THERE MUST BE EXACTLY TWO KNIGHTS."
   If R <> 2 Then Print "THERE MUST BE EXACTLY TWO ROOKS."
   If Not (pK(0) > pR(0)) And (pK(0) < pR(1)) Then Print "KING MUST BE BETWEEN THE ROOKS."
   If Not (pB(0) And 1) <> (pB(1) And 1) Then Print "BISHOPS MUST BE ON OPPOSITE COLORS."
   For i = 0 To 1
       N = pN(i)
       If pN(i) > pQ(i) Then N -= 1
       For j = 0 To 1
           If pN(i) > pB(j) Then N -= 1
       Next j
       pN(i) = N
   Next i
   Dim As Integer N0 = 1, N1 = 2
   For N = 0 To 9
       If N0 = pN(0) And N1 = pN(1) Then Exit For
       N1 += 1
       If N1 > 5 Then N0 += 1: N1 = N0 + 1
   Next N
   Q = pQ(0) - 1
   For i = 0 To 1
       If pQ(0) > pB(i) Then Q -= 1
   Next i
   For i = 0 To 1
       B = pB(i) - 1
       If B And 1 Then L = Int(B / 2)
       If (B And 1) = 0 Then D = B / 2
   Next i
   
   Print PosicPiezas; " has SP_ID of"; 96 * N + 16 * Q + 4 * D + L

End Sub

SP_ID("QNRBBNKR") Print SP_ID("RNBQKBNR") Print SP_ID("RQNBBKRN") Print SP_ID("RNQBBKRN") Sleep</lang>

Output:
QNRBBNKR has SP_ID of 105

RNBQKBNR has SP_ID of 518

RQNBBKRN has SP_ID of 601

RNQBBKRN has SP_ID of 617

QBasic

Works with: QBasic version 1.1
Translation of: Commodore BASIC

<lang qbasic>CLS PRINT "ENTER START ARRAY AS SEEN BY WHITE." 120 PRINT PRINT "STARTING ARRAY:"; INPUT AR$ PRINT IF LEN(AR$) = 0 THEN END IF LEN(AR$) = 8 THEN 170 PRINT "ARRAY MUST BE 8 PIECES.": GOTO 120

170 FOR I = 1 TO 8 P$ = MID$(AR$, I, 1) IF P$ = "Q" THEN Q(Q) = I: Q = Q + 1: GOTO 250 IF P$ = "K" THEN K(K) = I: K = K + 1: GOTO 250 IF P$ = "B" THEN B(B) = I: B = B + 1: GOTO 250 IF P$ = "N" THEN N(N) = I: N = N + 1: GOTO 250 IF P$ = "R" THEN R(R) = I: R = R + 1: GOTO 250 PRINT "ILLEGAL PIECE '"; P$; "'.": GOTO 120 250 NEXT I

IF K <> 1 THEN PRINT "THERE MUST BE EXACTLY ONE KING.": GOTO 120 IF Q <> 1 THEN PRINT "THERE MUST BE EXACTLY ONE QUEEN.": GOTO 120 IF B <> 2 THEN PRINT "THERE MUST BE EXACTLY TWO BISHOPS.": GOTO 120 IF N <> 2 THEN PRINT "THERE MUST BE EXACTLY TWO KNIGHTS.": GOTO 120 IF R <> 2 THEN PRINT "THERE MUST BE EXACTLY TWO ROOKS.": GOTO 120 IF (K(0) > R(0)) AND (K(0) < R(1)) THEN 330 PRINT "KING MUST BE BETWEEN THE ROOKS.": GOTO 120

330 IF (B(0) AND 1) <> (B(1) AND 1) THEN 350 PRINT "BISHOPS MUST BE ON OPPOSITE COLORS.": GOTO 120

350 FOR I = 0 TO 1 N = N(I) IF N(I) > Q(I) THEN N = N - 1 FOR J = 0 TO 1

   IF N(I) > B(J) THEN N = N - 1

NEXT J N(I) = N NEXT I N0 = 1: N1 = 2

FOR N = 0 TO 9

   IF N0 = N(0) AND N1 = N(1) THEN 490
   N1 = N1 + 1
   IF N1 > 5 THEN N0 = N0 + 1: N1 = N0 + 1

NEXT N 490 Q = Q(0) - 1

FOR I = 0 TO 1

   IF Q(0) > B(I) THEN Q = Q - 1

NEXT I

FOR I = 0 TO 1

   B = B(I) - 1
   IF B AND 1 THEN L = INT(B / 2)
   IF (B AND 1) = 0 THEN D = B / 2

NEXT I PRINT "SPID ="; 96 * N + 16 * Q + 4 * D + L END</lang>

Output:
Igual que la entrada de Commodore BASIC.

Common Lisp

<lang lisp>(defun sp-id (start-array)

  (let* ((n5n-table   '("NN---" "N-N--" "N--N-" "N---N" "-NN--" "-N-N-" "-N--N" "--NN-" "--N-N" "---NN"))
         (n5n-pattern  (substitute-if-not #\- (lambda (ch) (eql ch #\N)) (remove #\Q (remove #\B start-array))))
         (knights      (position n5n-pattern n5n-table :test #'string-equal))
         (queen        (position #\Q (remove #\B start-array)))
         (left-bishop  (position #\B start-array))
         (right-bishop (position #\B start-array :from-end t)))
   (destructuring-bind (dark-bishop light-bishop) (mapcar (lambda (p) (floor p 2))
       (cond ((zerop (mod left-bishop 2)) (list left-bishop  right-bishop))
             (t                           (list right-bishop left-bishop))))
       (+ (* 96 knights) (* 16 queen) (* 4 dark-bishop) light-bishop))))

(loop for ary in '("RNBQKBNR""QNRBBNKR""RQNBBKRN""RNQBBKRN") doing

 (format t "~a: ~a~%" ary (sp-id ary)))

</lang>

Output:
RNBQKBNR: 518
QNRBBNKR: 105
RQNBBKRN: 601
RNQBBKRN: 617

Factor

Works with: Factor version 0.99 2021-06-02

<lang factor>USING: assocs assocs.extras combinators formatting kernel literals math math.combinatorics sequences sequences.extras sets strings ;

IN: scratchpad

! ====== optional error-checking ======

check-length ( str -- )
   length 8 = [ "Must have 8 pieces." throw ] unless ;
check-one ( str -- )
   "KQ" counts [ nip 1 = not ] assoc-find nip
   [ 1string "Must have one %s." sprintf throw ] [ drop ] if ;
check-two ( str -- )
   "BNR" counts [ nip 2 = not ] assoc-find nip
   [ 1string "Must have two %s." sprintf throw ] [ drop ] if ;
check-king ( str -- )
   "QBN" without "RKR" =
   [ "King must be between rooks." throw ] unless ;
check-bishops ( str -- )
   CHAR: B swap indices sum odd?
   [ "Bishops must be on opposite colors." throw ] unless ;
check-sp ( str -- )
   {
       [ check-length ]
       [ check-one ]
       [ check-two ]
       [ check-king ]
       [ check-bishops ]
   } cleave ;

! ====== end optional error-checking ======


CONSTANT: convert $[ "RNBQK" "♖♘♗♕♔" zip ]

CONSTANT: table $[ "NN---" all-unique-permutations ]

knightify ( str -- newstr )
   [ dup CHAR: N = [ drop CHAR: - ] unless ] map ;
n ( str -- n ) "QB" without knightify table index ;
q ( str -- q ) "B" without CHAR: Q swap index ;
d ( str -- d ) CHAR: B swap <evens> index ;
l ( str -- l ) CHAR: B swap <odds> index ;
sp-id ( str -- n )
   dup check-sp
   { [ n 96 * ] [ q 16 * + ] [ d 4 * + ] [ l + ] } cleave ;
sp-id. ( str -- )
   dup [ convert substitute ] [ sp-id ] bi
   "%s / %s: %d\n" printf ;

"QNRBBNKR" sp-id. "RNBQKBNR" sp-id. "RQNBBKRN" sp-id. "RNQBBKRN" sp-id.</lang>

Output:
QNRBBNKR / ♕♘♖♗♗♘♔♖: 105
RNBQKBNR / ♖♘♗♕♔♗♘♖: 518
RQNBBKRN / ♖♕♘♗♗♔♖♘: 601
RNQBBKRN / ♖♘♕♗♗♔♖♘: 617

Go

Translation of: Wren

<lang go>package main

import (

   "fmt"
   "log"
   "strings"

)

var glyphs = []rune("♜♞♝♛♚♖♘♗♕♔") var names = map[rune]string{'R': "rook", 'N': "knight", 'B': "bishop", 'Q': "queen", 'K': "king"} var g2lMap = map[rune]string{

   '♜': "R", '♞': "N", '♝': "B", '♛': "Q", '♚': "K",
   '♖': "R", '♘': "N", '♗': "B", '♕': "Q", '♔': "K",

}

var ntable = map[string]int{"01": 0, "02": 1, "03": 2, "04": 3, "12": 4, "13": 5, "14": 6, "23": 7, "24": 8, "34": 9}

func g2l(pieces string) string {

   lets := ""
   for _, p := range pieces {
       lets += g2lMap[p]
   }
   return lets

}

func spid(pieces string) int {

   pieces = g2l(pieces) // convert glyphs to letters
   /* check for errors */
   if len(pieces) != 8 {
       log.Fatal("There must be exactly 8 pieces.")
   }
   for _, one := range "KQ" {
       count := 0
       for _, p := range pieces {
           if p == one {
               count++
           }
       }
       if count != 1 {
           log.Fatalf("There must be one %s.", names[one])
       }
   }
   for _, two := range "RNB" {
       count := 0
       for _, p := range pieces {
           if p == two {
               count++
           }
       }
       if count != 2 {
           log.Fatalf("There must be two %s.", names[two])
       }
   }
   r1 := strings.Index(pieces, "R")
   r2 := strings.Index(pieces[r1+1:], "R") + r1 + 1
   k := strings.Index(pieces, "K")
   if k < r1 || k > r2 {
       log.Fatal("The king must be between the rooks.")
   }
   b1 := strings.Index(pieces, "B")
   b2 := strings.Index(pieces[b1+1:], "B") + b1 + 1
   if (b2-b1)%2 == 0 {
       log.Fatal("The bishops must be on opposite color squares.")
   }
   /* compute SP_ID */
   piecesN := strings.ReplaceAll(pieces, "Q", "")
   piecesN = strings.ReplaceAll(piecesN, "B", "")
   n1 := strings.Index(piecesN, "N")
   n2 := strings.Index(piecesN[n1+1:], "N") + n1 + 1
   np := fmt.Sprintf("%d%d", n1, n2)
   N := ntable[np]
   piecesQ := strings.ReplaceAll(pieces, "B", "")
   Q := strings.Index(piecesQ, "Q")
   D := strings.Index("0246", fmt.Sprintf("%d", b1))
   L := strings.Index("1357", fmt.Sprintf("%d", b2))
   if D == -1 {
       D = strings.Index("0246", fmt.Sprintf("%d", b2))
       L = strings.Index("1357", fmt.Sprintf("%d", b1))
   }
   return 96*N + 16*Q + 4*D + L

}

func main() {

   for _, pieces := range []string{"♕♘♖♗♗♘♔♖", "♖♘♗♕♔♗♘♖", "♖♕♘♗♗♔♖♘", "♖♘♕♗♗♔♖♘"} {
       fmt.Printf("%s or %s has SP-ID of %d\n", pieces, g2l(pieces), spid(pieces))
   }

}</lang>

Output:
♕♘♖♗♗♘♔♖ or QNRBBNKR has SP-ID of 105
♖♘♗♕♔♗♘♖ or RNBQKBNR has SP-ID of 518
♖♕♘♗♗♔♖♘ or RQNBBKRN has SP-ID of 601
♖♘♕♗♗♔♖♘ or RNQBBKRN has SP-ID of 617

J

Implementation:<lang J>REF=: Template:'N Q B0 B1'=. 0 6 4 4"0 i.960

c960=: {{ r=. REF i. rplc&((u:9812+i.12);&>12$'KQRBNP') 7 u:deb y assert. r<#REF }}</lang>

Examples: <lang J> c960'♕♘♖♗♗♘♔♖' 105

  c960'♛♞♜♝♝♞♚♜'

105

  c960'RNBQKBNR'

518

  c960'RQNBBKRN'

601

  c960'RNQBBKRN'

617</lang>

Julia

<lang julia>const whitepieces = "♖♘♗♕♔♗♘♖♙" const whitechars = "rnbqkp" const blackpieces = "♜♞♝♛♚♝♞♜♟" const blackchars = "RNBQKP" const piece2ascii = Dict(zip("♖♘♗♕♔♗♘♖♙♜♞♝♛♚♝♞♜♟", "rnbqkbnrpRNBQKBNRP"))

""" Derive a chess960 position's SP-ID from its string representation. """ function chess960spid(position::String = "♖♘♗♕♔♗♘♖", errorchecking = true)

   if errorchecking
       @assert length(position) == 8 "Need exactly 8 pieces"
       @assert all(p -> p in whitepieces || p in blackpieces, position) "Invalid piece character"
       @assert all(p -> p in whitepieces, position) || all(p -> p in blackpieces, position) "Side of pieces is mixed"
       @assert all(p -> !(p in "♙♟"), position) "No pawns allowed"
   end
   a = uppercase(String([piece2ascii[c] for c in position]))
   if errorchecking
       @assert all(p -> count(x -> x == p, a) == 1, "KQ") "Need exactly one of each K and Q"
       @assert all(p -> count(x -> x == p, a) == 2, "RNB") "Need exactly 2 of each R, N, B"
       @assert findfirst(p -> p == 'R', a) < findfirst(p -> p == 'K', a) < findlast(p -> p == 'R', a) "King must be between rooks"
       @assert isodd(findfirst(p -> p == 'B', a) + findlast(p -> p == 'B', a)) "Bishops must be on different colors"
   end
   knighttable = [12, 13, 14, 15, 23, 24, 25, 34, 35, 45]
   noQB = replace(a, r"[QB]" => "")
   knightpos1, knightpos2 = findfirst(c -> c =='N', noQB), findlast(c -> c =='N', noQB)
   N = findfirst(s -> s == 10 * knightpos1 + knightpos2, knighttable) - 1
   Q = findfirst(c -> c == 'Q', replace(a, "B" => "")) - 1
   bishoppositions = [findfirst(c -> c =='B', a), findlast(c -> c =='B', a)]
   if isodd(bishoppositions[2])
       bishoppositions = reverse(bishoppositions) # dark color bishop first
   end
   D, L = bishoppositions[1] ÷ 2, bishoppositions[2] ÷ 2 - 1
   return 96N + 16Q + 4D + L

end

for position in ["♕♘♖♗♗♘♔♖", "♖♘♗♕♔♗♘♖", "♖♕♘♗♗♔♖♘", "♖♘♕♗♗♔♖♘"]

   println(collect(position), " => ", chess960spid(position))

end

</lang>

Output:
['♕', '♘', '♖', '♗', '♗', '♘', '♔', '♖'] => 105
['♖', '♘', '♗', '♕', '♔', '♗', '♘', '♖'] => 518
['♖', '♕', '♘', '♗', '♗', '♔', '♖', '♘'] => 601
['♖', '♘', '♕', '♗', '♗', '♔', '♖', '♘'] => 617

Nim

Translation of: Wren

<lang Nim>import sequtils, strformat, strutils, sugar, tables, unicode

type Piece {.pure.} = enum Rook = "R", Knight = "N", Bishop = "B", Queen = "Q", King = "K"

const

 GlypthToPieces = {"♜": Rook, "♞": Knight, "♝": Bishop, "♛": Queen, "♚": King,
                   "♖": Rook, "♘": Knight, "♗": Bishop, "♕": Queen, "♔": King}.toTable
 Names = [Rook: "rook", Knight: "knight", Bishop: "bishop", Queen: "queen", King: "king"]
 NTable = {[0, 1]: 0, [0, 2]: 1, [0, 3]: 2, [0, 4]: 3, [1, 2]: 4,
           [1, 3]: 5, [1, 4]: 6, [2, 3]: 7, [2, 4]: 8, [3, 4]: 9}.toTable

func toPieces(glyphs: string): seq[Piece] =

 collect(newSeq, for glyph in glyphs.runes: GlypthToPieces[glyph.toUTF8])

func isEven(n: int): bool = (n and 1) == 0

func positions(pieces: seq[Piece]; piece: Piece): array[2, int] =

 var idx = 0
 for i, p in pieces:
   if p == piece:
     result[idx] = i
     inc idx

func spid(glyphs: string): int =

 let pieces = glyphs.toPieces()
 # Check for errors.
 if pieces.len != 8:
   raise newException(ValueError, "there must be exactly 8 pieces.")
 for piece in [King, Queen]:
   if pieces.count(piece) != 1:
     raise newException(ValueError, &"there must be one {Names[piece]}.")
 for piece in [Rook, Knight, Bishop]:
   if pieces.count(piece) != 2:
     raise newException(ValueError, &"there must be two {Names[piece]}s.")
 let r = pieces.positions(Rook)
 let k = pieces.find(King)
 if k < r[0] or k > r[1]:
   raise newException(ValueError, "the king must be between the rooks.")
 var b = pieces.positions(Bishop)
 if isEven(b[1] - b[0]):
   raise newException(ValueError, "the bishops must be on opposite color squares.")
 # Compute SP_ID.
 let piecesN = pieces.filterIt(it notin [Queen, Bishop])
 let n = NTable[piecesN.positions(Knight)]
 let piecesQ = pieces.filterIt(it != Bishop)
 let q = piecesQ.find(Queen)
 if b[1].isEven: swap b[0], b[1]
 let d = [0, 2, 4, 6].find(b[0])
 let l = [1, 3, 5, 7].find(b[1])
 result = 96 * n + 16 * q + 4 * d + l


for glyphs in ["♕♘♖♗♗♘♔♖", "♖♘♗♕♔♗♘♖"]:

 echo &"{glyphs} or {glyphs.toPieces().join()} has SP-ID of {glyphs.spid()}"</lang>
Output:
♕♘♖♗♗♘♔♖ or QNRBBNKR has SP-ID of 105
♖♘♗♕♔♗♘♖ or RNBQKBNR has SP-ID of 518
♖♕♘♗♗♔♖♘ or RQNBBKRN has SP-ID of 601
♖♘♕♗♗♔♖♘ or RNQBBKRN has SP-ID of 617

Perl

Translation of: Raku

<lang perl>use strict; use warnings; use feature 'say'; use List::AllUtils 'indexes';

sub sp_id {

   my $setup = shift // 'RNBQKBNR';
   8 == length $setup                          or die 'Illegal position: should have exactly eight pieces';
   1 == @{[ $setup =~ /$_/g ]}                 or die "Illegal position: should have exactly one $_"        for <K Q>;
   2 == @{[ $setup =~ /$_/g ]}                 or die "Illegal position: should have exactly two $_\'s"     for ;
   $setup =~ m/R .* K .* R/x                   or die 'Illegal position: King not between rooks.';
   index($setup,'B')%2 != rindex($setup,'B')%2 or die 'Illegal position: Bishops not on opposite colors.';
   my @knights = indexes { 'N' eq $_ } split , $setup =~ s/[QB]//gr;
   my $knight  = indexes { join(, @knights) eq $_ } <01 02 03 04 12 13 14 23 24 34>; # combinations(5,2)
   my @bishops = indexes { 'B' eq $_ } split , $setup;
   my $dark  = int ((grep { $_ % 2 == 0 } @bishops)[0]) / 2;
   my $light = int ((grep { $_ % 2 == 1 } @bishops)[0]) / 2;
   my $queen = index(($setup =~ s/B//gr), 'Q');
   int 4*(4*(6*$knight + $queen)+$dark)+$light;

}

say "$_ " . sp_id($_) for <QNRBBNKR RNBQKBNR RQNBBKRN RNQBBKRN>;</lang>

Output:
QNRBBNKR 105
RNBQKBNR 518
RQNBBKRN 601
RNQBBKRN 617

Phix

with javascript_semantics
function spid(string s)
    if sort(s)!="BBKNNQRR" then return -1 end if
    if filter(s,"in","RK")!="RKR" then return -1 end if
    sequence b = find_all('B',s)
    if even(sum(b)) then return -1 end if
    integer {n1,n2} = find_all('N',filter(s,"out","QB")),
            N = {-2,1,3,4}[n1]+n2,
            Q = find('Q',filter(s,"!=",'B'))-1,
            D = filter(b,odd)[1]-1, -- (nb not /2)
            L = filter(b,even)[1]/2-1
    return 96*N + 16*Q + 2*D + L
end function

procedure test(string s)
    printf(1,"%s : %d\n",{s,spid(s)})
end procedure

test("QNRBBNKR")
test("RNBQKBNR")
test("RQNBBKRN")
test("RNQBBKRN")
Output:
QNRBBNKR : 105
RNBQKBNR : 518
RQNBBKRN : 601
RNQBBKRN : 617

To support all those crazy unicode characters just change the start of spid() to:

function spid(string u)
    sequence u32 = utf8_to_utf32(u),
             c32 = utf8_to_utf32("♜♞♝♛♚♖♘♗♕♔"),
             s32 = substitute_all(u32,c32,"RNBQKRNBQK")
    string s = utf32_to_utf8(s32)

--... and add:
test("♕♘♖♗♗♘♔♖")
test("♖♘♗♕♔♗♘♖")
test("♜♛♞♝♝♚♜♞")
test("♜♞♛♝♝♚♜♞")
Output:

Note that output on a windows terminal is as expected far from pretty, this is from pwa/p2js

QNRBBNKR : 105
RNBQKBNR : 518
RQNBBKRN : 601
RNQBBKRN : 617
♕♘♖♗♗♘♔♖ : 105
♖♘♗♕♔♗♘♖ : 518
♜♛♞♝♝♚♜♞ : 601
♜♞♛♝♝♚♜♞ : 617

Python

Works with: Python version 3.10.5 2022-06-28

<lang python># optional, but task function depends on it as written def validate_position(candidate: str):

   assert (
       len(candidate) == 8
   ), f"candidate position has invalide len = {len(candidate)}"
   valid_pieces = {"R": 2, "N": 2, "B": 2, "Q": 1, "K": 1}
   assert {
       piece for piece in candidate
   } == valid_pieces.keys(), f"candidate position contains invalid pieces"
   for piece_type in valid_pieces.keys():
       assert (
           candidate.count(piece_type) == valid_pieces[piece_type]
       ), f"piece type '{piece_type}' has invalid count"
   bishops_pos = [index for index, 
                  value in enumerate(candidate) if value == "B"]
   assert (
       bishops_pos[0] % 2 != bishops_pos[1] % 2
   ), f"candidate position has both bishops in the same color"
   assert [piece for piece in candidate if piece in "RK"] == [
       "R",
       "K",
       "R",
   ], "candidate position has K outside of RR"


def calc_position(start_pos: str):

   try:
       validate_position(start_pos)
   except AssertionError:
       raise AssertionError
   # step 1
   subset_step1 = [piece for piece in start_pos if piece not in "QB"]
   nights_positions = [
       index for index, value in enumerate(subset_step1) if value == "N"
   ]
   nights_table = {
       (0, 1): 0,
       (0, 2): 1,
       (0, 3): 2,
       (0, 4): 3,
       (1, 2): 4,
       (1, 3): 5,
       (1, 4): 6,
       (2, 3): 7,
       (2, 4): 8,
       (3, 4): 9,
   }
   N = nights_table.get(tuple(nights_positions))
   # step 2
   subset_step2 = [piece for piece in start_pos if piece != "B"]
   Q = subset_step2.index("Q")
   # step 3
   dark_squares = [
       piece for index, piece in enumerate(start_pos) if index in range(0, 9, 2)
   ]
   light_squares = [
       piece for index, piece in enumerate(start_pos) if index in range(1, 9, 2)
   ]
   D = dark_squares.index("B")
   L = light_squares.index("B")
   return 4 * (4 * (6*N + Q) + D) + L

if __name__ == '__main__':

   for example in ["QNRBBNKR", "RNBQKBNR", "RQNBBKRN", "RNQBBKRN"]:
       print(f'Position: {example}; Chess960 PID= {calc_position(example)}')</lang>
Output:
Position: QNRBBNKR; Chess960 PID= 105
Position: RNBQKBNR; Chess960 PID= 518
Position: RQNBBKRN; Chess960 PID= 601
Position: RNQBBKRN; Chess960 PID= 617

Raku

<lang perl6>#!/usr/bin/env raku

  1. derive a chess960 position's SP-ID

unit sub MAIN($array = "♖♘♗♕♔♗♘♖");

  1. standardize on letters for easier processing

my $ascii = $array.trans("♜♞♝♛♚♖♘♗♕♔" => "RNBQKRNBQK");

  1. (optional error-checking)

if $ascii.chars != 8 {

   die "Illegal position: should have exactly eight pieces\n";

}

for «K Q» -> $one {

 if +$ascii.indices($one) != 1 {
   die "Illegal position: should have exactly one $one\n";
 }

}

for «B N R» -> $two {

 if +$ascii.indices($two) != 2 {
   die "Illegal position: should have exactly two $two\'s\n";
 }

}

if $ascii !~~ /'R' .* 'K' .* 'R'/ {

 die "Illegal position: King not between rooks.";

}

if [+]($ascii.indices('B').map(* % 2)) != 1 {

 die "Illegal position: Bishops not on opposite colors.";

}

  1. (end optional error-checking)
  1. Work backwards through the placement rules.
  2. King and rooks are forced during placement, so ignore them.
  1. 1. Figure out which knight combination was used:

my @knights = $ascii

 .subst(/<[QB]>/,,:g)
 .indices('N');

my $knight = combinations(5,2).kv.grep(

   -> $i,@c { @c eq @knights }
 )[0][0]; 
  1. 2. Then which queen position:

my $queen = $ascii

 .subst(/<[B]>/,,:g)
 .index('Q');
  1. 3. Finally the two bishops:

my @bishops = $ascii.indices('B'); my $dark = @bishops.grep({ $_ %% 2 })[0] div 2; my $light = @bishops.grep({ not $_ %% 2 })[0] div 2;

my $sp-id = 4*(4*(6*$knight + $queen)+$dark)+$light;

  1. standardize output

my $display = $ascii.trans("RNBQK" => "♖♘♗♕♔");

say "$display: $sp-id";</lang>

Output:
$ for demo in QNRBBNKR RNBQKBNR RQNBBKRN RNQBBKRN; do c960spid "$demo"; done
♕♘♖♗♗♘♔♖: 105
♖♘♗♕♔♗♘♖: 518
♖♕♘♗♗♔♖♘: 601
♖♘♕♗♗♔♖♘: 617

Ruby

<lang ruby>CHESS_PIECES = %w<♖♘♗♕♔ ♜♞♝♛♚> def chess960_to_spid(pos)

 start_str = pos.tr(CHESS_PIECES.join, "RNBQKRNBQK")
 #1 knights score
 s = start_str.delete("QB")
 n = [0,1,2,3,4].combination(2).to_a.index( [s.index("N"), s.rindex("N")] )
 #2 queen score
 q = start_str.delete("B").index("Q")
 #3 bishops
 bs = start_str.index("B"), start_str.rindex("B")
 d = bs.detect(&:even?).div(2)
 l = bs.detect(&:odd? ).div(2)
 96*n + 16*q + 4*d + l

end

%w<QNRBBNKR RNBQKBNR RQNBBKRN RNQBBKRN>.each_with_index do |array, i|

 pieces = array.tr("RNBQK", CHESS_PIECES[i%2])
 puts "#{pieces} (#{array}):  #{chess960_to_spid array}"

end </lang>

Output:
♕♘♖♗♗♘♔♖ (QNRBBNKR):  105
♜♞♝♛♚♝♞♜ (RNBQKBNR):  518
♖♕♘♗♗♔♖♘ (RQNBBKRN):  601
♜♞♛♝♝♚♜♞ (RNQBBKRN):  617

Wren

Library: Wren-trait

<lang ecmascript>import "/trait" for Indexed

var glyphs = "♜♞♝♛♚♖♘♗♕♔".toList var letters = "RNBQKRNBQK" var names = { "R": "rook", "N": "knight", "B": "bishop", "Q": "queen", "K": "king" } var g2lMap = {} for (se in Indexed.new(glyphs)) g2lMap[glyphs[se.index]] = letters[se.index]

var g2l = Fn.new { |pieces| pieces.reduce("") { |acc, p| acc + g2lMap[p] } }

var ntable = { "01":0, "02":1, "03":2, "04":3, "12":4, "13":5, "14":6, "23":7, "24":8, "34":9 }

var spid = Fn.new { |pieces|

   pieces = g2l.call(pieces) // convert glyphs to letters
   /* check for errors */
   if (pieces.count != 8) Fiber.abort("There must be exactly 8 pieces.")
   for (one in "KQ") {
       if (pieces.count { |p| p == one } != 1 ) Fiber.abort("There must be one %(names[one]).")
   }
   for (two in "RNB") {
       if (pieces.count { |p| p == two } != 2 ) Fiber.abort("There must be two %(names[two])s.")
   }
   var r1 = pieces.indexOf("R")
   var r2 = pieces.indexOf("R", r1 + 1)
   var k  = pieces.indexOf("K")
   if (k < r1 || k > r2) Fiber.abort("The king must be between the rooks.")
   var b1 = pieces.indexOf("B")
   var b2 = pieces.indexOf("B", b1 + 1)
   if ((b2 - b1) % 2 == 0) Fiber.abort("The bishops must be on opposite color squares.")
   /* compute SP_ID */
   var piecesN = pieces.replace("Q", "").replace("B", "")
   var n1 = piecesN.indexOf("N")
   var n2 = piecesN.indexOf("N", n1 + 1)
   var np = "%(n1)%(n2)"
   var N = ntable[np]
   var piecesQ = pieces.replace("B", "")
   var Q = piecesQ.indexOf("Q")
   var D = "0246".indexOf(b1.toString)
   var L = "1357".indexOf(b2.toString)
   if (D == -1) {
       D = "0246".indexOf(b2.toString)
       L = "1357".indexOf(b1.toString)
   }
   return 96*N + 16*Q + 4*D + L

}

for (pieces in ["♕♘♖♗♗♘♔♖", "♖♘♗♕♔♗♘♖", "♜♛♞♝♝♚♜♞", "♜♞♛♝♝♚♜♞"]) {

   System.print("%(pieces) or %(g2l.call(pieces)) has SP-ID of %(spid.call(pieces))")

}</lang>

Output:
♕♘♖♗♗♘♔♖ or QNRBBNKR has SP-ID of 105
♖♘♗♕♔♗♘♖ or RNBQKBNR has SP-ID of 518
♜♛♞♝♝♚♜♞ or RQNBBKRN has SP-ID of 601
♜♞♛♝♝♚♜♞ or RNQBBKRN has SP-ID of 617