Score a game of Kingdom Builder

16

1

I want to try a new form of code golf here. Similar to bonuses, not all parts of the challenge have to be completed, but each answer has to implement a subset of certain size (and there is a core that every answer has to implement). So besides the golfing this challenge also involves choosing a set of features that go well together.

The Rules

Kingdom Builder is a board game, played on a (pointy-top) hex grid. The board is made up of four (randomised) quadrants, each of which has 10x10 hex cells (so a full board will be 20x20). For the purposes of this challenge, each hex cell contains either water (W), mountain (M) a town (T), a castle (C) or is empty (.). So a quadrant could look like

. . W . . . . . . .
 . M W W . . . . . .
. M . . W . . . T .
 M M . W . . . . . .
. . M . W W . . . .
 . . . . . W W W W W
. T . . . . . . . .
 . . W . . C . . . .
. . W W . . . . M . 
 . . . . . . . M M .

The second row will always be offset to the right from the first row. Players 1 to 4 can place up to 40 settlements each on empty cells (following some rules which we will ignore for this challenge). A possible board at the end of the game is the following:

3 3 W . . . 4 . 4 . . 2 W . 4 . . 4 . 4
 3 M W W . 1 1 . . 4 2 W . 3 C 4 4 . . 4
3 M 2 2 W 1 1 1 T 3 2 W 4 3 . 1 4 . 4 .
 M M . W 2 2 . . . 2 2 W 3 . 1 1 1 . . .
. 4 M . W W 2 2 2 2 W W 3 . 1 4 . T . .
 . . . . . W W W W W . 3 C 1 . . 2 2 2 2
. T 1 1 1 1 . . 2 . . 4 . . . 2 2 M M M
 4 . W 4 . C 4 4 . . . . . . 2 M M M M M
. 4 W W . . . 4 M . . W . W . 2 2 2 M M
 . . . . . . . M M . . W W . . . . 2 M .
. . . 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 . 1
 M 3 3 . . . . . . . . 4 . T 2 . 2 4 1 .
M M . C . 4 . 4 . . . . . 1 2 4 2 1 1 .
 M . . 1 . 4 . . . . M M 1 2 . . 2 1 . .
. . . W 1 1 4 1 1 . . . 1 2 . . 2 W W W
 . . 1 1 W 1 T . 1 1 1 1 T . . 2 W . 4 .
. 1 1 W . 3 3 . . . . . . . . 2 W 4 C 3
 C 1 3 3 3 . 3 . 4 . 4 . 4 . . 2 W 1 1 M
4 3 3 4 . M 4 3 . . . . . . . 2 W . . .
 . . . 4 . M M 3 . . 4 4 . 4 . 2 W W . .

We'll label the quadrants like

1 2
3 4

Your task will be to score such a board. There is one core score which is always used, and 8 optional scores, 3 of which are chosen for each game. In the following, I'll describe all 9 scores and use the above setup as an example for how many points each player would get.

† There are 10 scores in the actual game, but I'll leave out two because no one wants to golf them.

The core score. A player gets 3 points for each Castle they have a settlement next to. Example scores: 18, 0, 15, 12.

The optional scores.

  1. A player gets 1 point for each horizontal row on which they have at least one settlement.

    Example scores: 14, 20, 12, 16.

  2. For each player, find the horizontal row on which they most of their settlements (pick any in case of a tie). A player gets 2 points for each settlement on that row.

    Example scores: 14 (row 16), 8 (row 4, 5 or 6), 28 (row 11), 10 (row 1).

  3. A player gets 1 point for each settlement that is build next to Water.

    Example scores: 13, 21, 10, 5.

  4. A player gets 1 point for each settlement next to a Mountain.

    Example scores: 4, 12, 8, 4.

  5. Count the settlements of each player in each quadrant. Per quadrant, the players with the largest number of settlements get 12 points each, the players with the second-largest number of settlements get 6 points each.

    Example scores: 18 (6 + 0 + 6 + 6), 36 (12 + 12 + 0 + 12), 12 (0 + 0 + 12 + 0), 18 (12 + 6 + 0 + 0).

  6. For each player determine the quadrant in which they have the least number of settlements. A player gets 3 points for each settlement in that quadrant.

    Example scores: 18 (Quadrant 2), 0 (Quadrant 3), 15 (Quadrant 1 or 2), 27 (Quadrant 3).

  7. A player gets 1 point for each connected group of settlements.

    Example scores: 7, 5, 6, 29.

  8. A player gets 1 point for every 2 settlements in the player's largest group of connected settlements.

    Example scores: 4, 10, 8, 2.

The Challenge

As in the game you will choose 3 of the optional scores, and score a given board based on the core score and those three scores. Your code should produce a list of 4 scores. There is one restriction on the choice though: I have grouped the scores into 3 groups, and you are to implement one of each group:

  • Implement one of 1 and 2.
  • Implement one of 3, 4, 5 and 6.
  • Implement one of 7 and 8.

You may write a program or function, taking input via STDIN, command-line argument, prompt or function parameter. You may return the result or print it to STDOUT.

You may choose any convenient 1D or 2D list/string format for the input. You may not use a graph with full adjacency information. Here is some good reading on hex grids if you need inspiration.

Your output may also be in any convenient, unambiguous list or string format.

This is code golf, so the shortest answer (in bytes) wins.

Further Assumptions

You may assume that ...

  • ... each player has at least 1 settlement and there are no more than 40 settlements of each player.
  • ... each quadrant contains either one town and two castles, or two towns and one castle.
  • ... towns and castles are far enough apart, such that no settlement can be adjacent to two of them.

Test Cases

Still using the above board, here are the individual scores for all possible choices of scoring mechanisms:

Chosen Scores      Total Player Scores
1 3 7              52 46 43 62
1 3 8              49 51 45 35
1 4 7              43 37 41 61
1 4 8              40 42 43 34
1 5 7              57 61 45 75
1 5 8              54 66 47 48
1 6 7              57 25 48 84
1 6 8              54 30 50 57
2 3 7              52 34 59 56
2 3 8              49 39 61 29
2 4 7              43 25 57 55
2 4 8              40 30 59 28
2 5 7              57 49 61 69
2 5 8              54 54 63 42
2 6 7              57 13 64 78
2 6 8              54 18 66 51

Martin Ender

Posted 2015-01-12T17:21:18.330

Reputation: 184 808

Is there a board in which one player always wins, regardless of the combination? – ThreeFx – 2015-01-13T12:52:12.117

@ThreeFx Since the lower bound on the number of settlements per player is 1, that's fairly simple to set up. ;) But with the same number of settlements for each player, I don't actually know. – Martin Ender – 2015-01-13T12:54:37.140

Answers

5

Python 2, 367 bytes

T=range(20)
N=lambda r,c:{(a,b)for a,b in{(r+x/3-1,c+x%3-1+(x/3!=1)*r%2)for x in[0,1,3,5,6,7]}if-1<b<20>a>-1}
def S(B):
 def F(r,c):j=J[r][c]!=i;J[r][c]*=j;j or map(F,*zip(*N(r,c)));return j
 J=map(list,B);X=lambda r,c,x,y:x+y in{B[r][c]+B[a][b]for a,b in N(r,c)};return[sum((i in B[r])+20*(3*X(r,c,"C",i)-~X(r,c,i,"W")-F(r,c))for r in T for c in T)/20for i in"1234"]

The program uses scores 1, 3, 7. Input is a list of lists of chars representing each cell. To test the example board easily, we can do:

board = """
3 3 W . . . 4 . 4 . . 2 W . 4 . . 4 . 4
 3 M W W . 1 1 . . 4 2 W . 3 C 4 4 . . 4
3 M 2 2 W 1 1 1 T 3 2 W 4 3 . 1 4 . 4 .
 M M . W 2 2 . . . 2 2 W 3 . 1 1 1 . . .
. 4 M . W W 2 2 2 2 W W 3 . 1 4 . T . .
 . . . . . W W W W W . 3 C 1 . . 2 2 2 2
. T 1 1 1 1 . . 2 . . 4 . . . 2 2 M M M
 4 . W 4 . C 4 4 . . . . . . 2 M M M M M
. 4 W W . . . 4 M . . W . W . 2 2 2 M M
 . . . . . . . M M . . W W . . . . 2 M .
. . . 3 3 3 3 3 3 3 3 3 3 3 3 3 3 2 . 1
 M 3 3 . . . . . . . . 4 . T 2 . 2 4 1 .
M M . C . 4 . 4 . . . . . 1 2 4 2 1 1 .
 M . . 1 . 4 . . . . M M 1 2 . . 2 1 . .
. . . W 1 1 4 1 1 . . . 1 2 . . 2 W W W
 . . 1 1 W 1 T . 1 1 1 1 T . . 2 W . 4 .
. 1 1 W . 3 3 . . . . . . . . 2 W 4 C 3
 C 1 3 3 3 . 3 . 4 . 4 . 4 . . 2 W 1 1 M
4 3 3 4 . M 4 3 . . . . . . . 2 W . . .
 . . . 4 . M M 3 . . 4 4 . 4 . 2 W W . .
"""

board = [row.split() for row in board.strip().split("\n")]
print S(board)

# [52, 46, 43, 62]

Handling the hex grid

Since we are on a hex grid, we have to deal with neighbours a little differently. If we use a traditional 2D grid as our representation, then for (1, 1) we have:

. N N . .       . N N . .                (0, 1), (0, 2)            (-1, 0), (-1, 1)
 N X N . .  ->  N X N . .  -> Neighbours (1, 0), (1, 2) -> Offsets (0, -1), (0, 1)
. N N . .       . N N . .                (2, 1), (2, 2)            (1, 0), (1, 1)

On closer inspection, we realise that the offsets depend on the parity of the row you're on. The above example is for odd rows, but on even rows the offsets are

(-1, -1), (-1, 0), (0, -1), (0, 1), (1, -1), (1, 0)

The only thing that has changed is that the 1st, 2nd, 5th and 6th pairs have had their second coordinate decremented by 1.

The lambda function N takes a coordinate pair (row, col) and returns all neighbours of the cell within the grid. The inner comprehension generates the above offsets by extracting them from a simple base 3 encoding, incrementing the second coordinate if the row is odd and adds the offsets to the cell in question to give the neighbours. The outer comprehension then filters, leaving just the neighbours that are within the bounds of the grid.

Ungolfed

def neighbours(row, col):
    neighbour_set = set()

    for dr, dc in {(-1,-1), (-1,0), (0,-1), (0,1), (1,-1), (1,0)}:
        neighbour_set.add((row + dr, col + dc + (1 if dr != 0 and row%2 == 1 else 0)))

    return {(r,c) for r,c in neighbour_set if 20>r>-1 and 20>c>-1}

def solve(board):
    def flood_fill(char, row, col):
        # Logic negated in golfed code to save a few bytes
        is_char = (dummy[row][col] == char)
        dummy[row][col] = "" if is_char else dummy[row][col]

        if is_char:
            for neighbour in neighbours(row, col):
                flood_fill(char, *neighbour)

        return is_char

    def neighbour_check(row, col, char1, char2):
        return board[row][col] == char1 and char2 in {board[r][c] for r,c in neighbours(row, col)}

    dummy = [row[:] for row in board] # Need to deep copy for the flood fill
    scores = [0]*4

    for i,char in enumerate("1234"):
        for row in range(20):
            for col in range(20):
                scores[i] += (char in board[row])                        # Score 1
                scores[i] += 20 * 3*neighbour_check(row, col, "C", char) # Core score
                scores[i] += 20 * neighbour_check(row, col, char, "W")   # Score 3
                scores[i] += 20 * flood_fill(char, row, col)             # Score 7

        # Overcounted everything 20 times, divide out
        scores[i] /= 20

    return scores

Sp3000

Posted 2015-01-12T17:21:18.330

Reputation: 58 729

Can't the def F be a separate function rather than an internal function? Can't k be removed from def F:? – Justin – 2015-01-13T05:46:05.067

@Quincunx F is the flood fill function and needs access to J, so it's on the inside to save on passing J as a parameter (I'll experiment a bit to see if I can get around the deep copying). You're right about k though, thanks :) (the new code looks a little funky however, due to relying on short circuiting) – Sp3000 – 2015-01-13T06:04:07.860

2

Answer Set Programming, 629 bytes

d(X,Y):-b(X,Y,_).p(1;2;3;4).n(X,Y,(((X-2;X+2),Y);((X-1;X+1),(Y-1;Y+1)))):-d(X,Y).n(X,Y,I,J):-n(X,Y,(I,J));d(I,J).t(X,Y,P):-n(X,Y,I,J);b(I,J,P).s(c,P,S*3):-S={t(X,Y,P):b(X,Y,"C")};p(P).s(1,P,S*1):-S=#count{r(Y):b(_,Y,P)};p(P).s(3,P,S):-S={b(X,Y,P):t(X,Y,"W")};p(P).o(X,Y,Y+X*100):-d(X,Y).h(P,X,Y,I,J):-o(X,Y,O);o(I,J,Q);O<Q;n(X,Y,I,J);b(X,Y,P);b(I,J,P);p(P).h(P,X,Y,I,J):-o(X,Y,O);o(I,J,Q);O<Q;h(P,X,Y,K,L);n(K,L,I,J);b(I,J,P);p(P).c(P,X,Y):-h(P,X,Y,_,_);not h(P,_,_,X,Y).c(P,X,Y):-{h(P,X,Y,_,_);h(P,_,_,X,Y)}0;b(X,Y,P);p(P).s(7,P,S):-S=#count{c(P,X,Y):c(P,X,Y)};p(P).s(t,P,C+S+T+U):-s(c,P,C);s(1,P,S);s(3,P,T);s(7,P,U).#shows/3.

ASP belongs to the logic programming language family, here incarnated by the Potassco framework, in particular Clingo(grounder Gringo + solver Clasp). Because of the paradigm limitation, it can't take given board directly as an output, so a preprocessing of the data is necessary (here performed in python). This preprocessing in not counted in the total byte score.

Its my first code golf, and the objective is more to show a language i love that i never seen before in golf, than really win the game. Moreover, i'm far from an expert in ASP, so many optimizations of the code can certainly be perform for results in less bytes.

knowledge representation

There is the python code that convert board in atoms :

def asp_str(v):
    return ('"' + str(v) + '"') if v not in '1234' else str(v)

with open('board.txt') as fd, open('board.lp', 'w') as fo:
        [fo.write('b('+ str(x) +','+ str(y) +','+ asp_str(v) +').\n')
         for y, line in enumerate(fd)
         for x, v in enumerate(line) if v not in ' .\n'
        ]

For example, atoms b (for __b__oard) given for the first line of example board are the followings :

b(0,0,3).
b(2,0,3).
b(4,0,"W").
b(12,0,4).
b(16,0,4).
b(22,0,2).
b(24,0,"W").
b(28,0,4).
b(34,0,4).
b(38,0,4).

Where b(0,0,3) is an atom that describes that player 3 has a settlement at coordinates (0;0).

ASP solving

There is the ASP code, with many optional scores implemented :

% input : b(X,Y,V) with X,Y the coordinates of the V value

domain(X,Y):- b(X,Y,_).
player("1";"2";"3";"4").

% neighbors of X,Y
neighbors(X,Y,((X-2,Y);(X+2,Y);((X-1;X+1),(Y-1;Y+1)))) :- domain(X,Y).
neighbors(X,Y,I,J):- neighbors(X,Y,(I,J)) ; domain(I,J).

% Player is next to X,Y iff has a settlement next to.
next(X,Y,P):- neighbors(X,Y,I,J) ; b(I,J,P).


% SCORES

% Core score : 3 point for each Castle "C" with at least one settlement next to.
score(core,P,S*3):- S={next(X,Y,P): b(X,Y,"C")} ; player(P).

% opt1: 1 point per settled row
score(opt1,P,S*1):- S=#count{row(Y): b(_,Y,P)} ; player(P).

% opt2: 2 point per settlement on the most self-populated row
% first, defines how many settlements have a player on each row
rowcount(P,Y,H):- H=#count{col(X): b(X,Y,P)} ; domain(_,Y) ; player(P).
score(opt2,P,S*2):- S=#max{T: rowcount(P,Y,T)} ; player(P).

% opt3: 1 point for each settlements next to a Water "W".
score(opt3,P,S):- S={b(X,Y,P): next(X,Y,"W")} ; player(P).

% opt4: 1 point for each settlements next to a Mountain "M".
score(opt4,P,S):- S={b(X,Y,P): next(X,Y,"M")} ; player(P).

% opt5:
%later…

% opt6:
%later…

% opt7: 1 point for each connected component of settlement
% first we need each coord X,Y to be orderable.
% then is defined path/5, that is true iff exists a connected component of settlement of player P
%   that links X,Y to I,J
% then is defined the connected component atom that give the smaller coords in each connected component
% then computing the score.
order(X,Y,Y+X*100):- domain(X,Y).
path(P,X,Y,I,J):- order(X,Y,O1) ; order(I,J,O2) ; O1<O2 ; % order
                  neighbors(X,Y,I,J) ; b(X,Y,P) ; b(I,J,P) ; player(P). % path iff next to
path(P,X,Y,I,J):- order(X,Y,O1) ; order(I,J,O2) ; O1<O2 ; % order
                  path(P,X,Y,K,L) ; neighbors(K,L,I,J) ; % path if path to next to
                  b(I,J,P) ; player(P).
concomp(P,X,Y):- path(P,X,Y,_,_) ; not path(P,_,_,X,Y). % at least two settlements in the connected component
concomp(P,X,Y):- 0 { path(P,X,Y,_,_) ; path(P,_,_,X,Y) } 0 ; board(X,Y,P) ; player(P). % concomp of only one settlements
score(opt7,P,S):- S=#count{concomp(P,X,Y): concomp(P,X,Y)} ; player(P).

% opt8: 0.5 point for each settlement in the bigger connected component
%later…


% total score:
score(total,P,C+S1+S2+S3):- score(core,P,C) ; score(opt1,P,S1) ; score(opt3,P,S2) ; score(opt7,P,S3).

#show. # show nothing but the others show statements
#show total_score(P,S): score(total,P,S).
%#show score/3. % scores details

This program can be launched with the command:

clingo board.lp golf.lp 

And will find only one solution (its a proof that there is only one way to distribute the points):

s(c,1,18) s(c,2,0) s(c,3,15) s(c,4,12) s(1,1,14) s(1,2,20) s(1,3,12) s(1,4,16) s(3,1,13) s(3,2,21) s(3,3,10) s(3,4,5) s(7,1,7) s(7,2,5) s(7,3,6) s(7,4,29) s(t,1,52) s(t,2,46) s(t,3,43) s(t,4,62)

Where s(7,3,6) says that player 3 gains 6 points with optional score 7, and s(t,4,62) says that player 4 gains 62 points in total (core + 1 + 3 + 7).

Easy to parse to have a fancy table !

aluriak

Posted 2015-01-12T17:21:18.330

Reputation: 141