Magic: Golf Gathering Combat


30

Magic: the Gathering là một trò chơi đánh bài trong đó, trong số những thứ khác, người chơi chơi bài đại diện cho sinh vật, sau đó có thể tấn công người chơi khác hoặc phòng thủ trước các cuộc tấn công của người chơi khác bằng cách chặn.

Trong thử thách chơi gôn mã này, chương trình của bạn sẽ ở vị trí của một người chơi Magic quyết định cách chặn trong chiến đấu.


Mỗi sinh vật có hai thuộc tính có liên quan: Sức mạnh và độ dẻo dai. Sức mạnh của một sinh vật là lượng sát thương mà nó có thể gây ra trong một trận chiến, và độ dẻo dai của nó là lượng sát thương cần thiết để tiêu diệt nó. Sức mạnh luôn luôn ít nhất là 0 và độ dẻo dai luôn luôn ít nhất là 1.

Trong trận chiến trong Magic, người chơi đến lượt mình tuyên bố một số sinh vật của họ sẽ tấn công đối thủ. Sau đó, người chơi khác, được gọi là người chơi phòng thủ, có thể chỉ định sinh vật của họ là người chặn. Một sinh vật có thể chỉ chặn một sinh vật trong mỗi trận chiến, nhưng nhiều sinh vật có thể chặn cùng một sinh vật.

Sau khi các công cụ chặn được tuyên bố, người chơi tấn công quyết định, đối với mỗi sinh vật tấn công bị chặn, làm thế nào để phân phối thiệt hại (bằng với sức mạnh của nó) mà sinh vật đó gây ra cho các sinh vật chặn nó.

Sau đó, thiệt hại được xử lý. Mỗi sinh vật gây sát thương tương đương với sức mạnh của nó. Tấn công các sinh vật đã bị chặn gây sát thương như đã giải thích ở trên. Sinh vật tấn công không bị chặn gây sát thương cho người chơi phòng thủ. Chặn sinh vật gây sát thương cho sinh vật mà chúng chặn. Sinh vật thuộc về người chơi phòng thủ không chặn gây sát thương. (Sinh vật không bắt buộc phải chặn.)

Cuối cùng, bất kỳ sinh vật nào gây sát thương bằng hoặc lớn hơn độ dẻo dai của nó đều bị phá hủy, và bị loại khỏi chiến trường. Bất kỳ lượng sát thương nào nhỏ hơn độ dẻo dai của sinh vật đều không có tác dụng.


Đây là một ví dụ về quy trình này:

Một sinh vật có sức mạnh P và độ dẻo dai T được thể hiện là P/T

Attacking:
2/2, 3/3
Defending player's creatures:
1/4, 1/1, 0/1
Defending player declares blockers:
1/4 and 1/1 block 2/2, 0/1 does not block.
Attacking player distributes damage:
2/2 deals 1 damage to 1/4 and 1 damage to 1/1
Damage is dealt:
2/2 takes 2 damage, destroyed.
3/3 takes 0 damage.
1/1 takes 1 damage, destroyed.
1/4 takes 1 damage.
0/1 takes 0 damage.
Defending player is dealt 3 damage.

Người chơi phòng thủ có 3 mục tiêu trong chiến đấu: Tiêu diệt sinh vật của đối thủ, bảo tồn sinh vật của chính mình và bị gây sát thương ít nhất có thể. Ngoài ra, những sinh vật có sức mạnh và sự dẻo dai hơn có giá trị hơn.

Để kết hợp những điều này thành một biện pháp duy nhất, chúng tôi sẽ nói rằng điểm số của người chơi phòng thủ từ một trận chiến bằng tổng sức mạnh và sự dẻo dai của các sinh vật còn sống của họ, trừ đi sức mạnh và sự dẻo dai của các sinh vật còn sống của đối thủ, trừ đi một một nửa lượng sát thương gây ra cho người chơi phòng thủ. Người chơi phòng thủ muốn tối đa hóa điểm số này, trong khi người chơi tấn công muốn tối thiểu hóa nó.

Trong trận chiến hiển thị ở trên, điểm số là:

Defending player's surviving creatures:
1/4, 0/1
1 + 4 + 0 + 1 = 6
Attacking player's surviving creature:
3/3
3 + 3 = 6
Damage dealt to defending player:
3
6 - 6 - 3/2 = -1.5

Nếu người chơi phòng thủ hoàn toàn không bị chặn trong trận chiến được mô tả ở trên, điểm số sẽ là

8 - 10 - (5/2) = -4.5

Sự lựa chọn tối ưu cho người chơi phòng thủ sẽ là chặn 2/2với 1/11/4và chặn 3/3với 0/1. Nếu họ đã làm như vậy, chỉ 1/43/3sẽ sống sót, và không có thiệt hại nào sẽ được xử lý cho người chơi phòng thủ, tạo ra điểm số

5 - 6 - (0/2) = -1

Thử thách của bạn là viết một chương trình sẽ đưa ra lựa chọn chặn tối ưu cho người chơi phòng thủ. "Tối ưu" có nghĩa là lựa chọn tối đa hóa điểm số, cho rằng đối thủ phân phối thiệt hại theo cách giảm thiểu điểm số, dựa trên cách bạn chặn.

Đây là một maximin: Điểm tối đa trên các phân phối thiệt hại giúp giảm thiểu điểm số cho mỗi kết hợp chặn.


Đầu vào: Đầu vào sẽ bao gồm hai danh sách 2 tuple, trong đó mỗi 2 tuple có dạng (Power, Toughness). Danh sách đầu tiên sẽ chứa sức mạnh và độ dẻo dai của từng sinh vật tấn công (bạn là sinh vật của đối thủ). Danh sách thứ hai sẽ chứa sức mạnh và sự dẻo dai của mỗi sinh vật của bạn.

Các bộ và danh sách có thể được trình bày theo bất kỳ định dạng thuận tiện nào, chẳng hạn như:

[[2, 2], [3, 3]]
[[1, 4], [1, 1], [0, 1]]

Đầu ra: Đầu ra sẽ bao gồm một loạt 2 tuple, ở dạng (sinh vật chặn, sinh vật bị chặn) - nghĩa là, một trong những sinh vật của bạn theo sau là một trong những sinh vật của họ. Các sinh vật sẽ được giới thiệu bởi chỉ mục của chúng trong danh sách đầu vào. Các chỉ mục có thể là 0 hoặc 1 được lập chỉ mục. Một lần nữa, bất kỳ định dạng thuận tiện. Bất kỳ thứ tự là tốt. Ví dụ: kịch bản chặn tối ưu từ phía trên, với đầu vào ở trên, có thể được biểu diễn dưới dạng:

[0, 0]    # 1/4 blocks 2/2
[1, 0]    # 1/1 blocks 2/2
[2, 1]    # 0/1 blocks 3/3

Ví dụ:

Input:
[[2, 2], [3, 3]]
[[1, 4], [1, 1], [0, 1]]
Output:
[0, 0]
[1, 0]
[2, 1]

Input:
[[3, 3], [3, 3]]
[[2, 3], [2, 2], [2, 2]]
Output:
[1, 0]
[2, 0]
or
[1, 1]
[2, 1]

Input:
[[3, 1], [7, 2]]
[[0, 4], [1, 1]]
Output:
[1, 0]
or
[0, 0]
[1, 0]

Input:
[[2, 2]]
[[1, 1]]
Output:

(No output tuples).

Đầu vào và đầu ra có thể qua STDIN, STDOUT, CLA, chức năng đầu vào / trở lại vv sơ hở tiêu chuẩn được áp dụng. Đây là code-golf: mã ngắn nhất tính theo byte thắng.


Để làm rõ thông số kỹ thuật và cung cấp các ý tưởng ban đầu, pastebin này cung cấp một giải pháp tham chiếu trong Python. Các best_blockchức năng là một giải pháp mẫu để thử thách này, và chạy chương trình sẽ cung cấp đầu vào và đầu ra dài hơn.


18
Bạn nên làm vua của ngọn đồi này.
PyRulez

1
@Arnauld không, đó cũng là một câu trả lời hợp lệ.
isaacg

Câu trả lời:


6

JavaScript (ES7),  354  348 byte

Đưa đầu vào là ([attackers], [defenders]).

(a,d,O,M)=>eval(`for(N=(A=a.push([,0]))**d.length;N--;)O=a[X='map'](([P,T],i)=>S-=((g=(n,l)=>n?l[X](([t,S],i)=>g(n-1,b=[...l],b[i]=[t-1,S])):m=l[X](([t,S])=>s+=t>0&&S,s=0)&&s>m?m:s)(P,l[n=0,i][X](m=([p,t])=>[t,p+t,n+=p])),n<T&&P+T)+(l[i]<1?T/2:-m),S=0,d[X]((x,i)=>l[(j=N/A**i%A|0)<A-1&&o.push([i,j]),j].push(x),o=[],l=a[X](_=>[])))&&S<M?O:(M=S,o)`)

Hãy thử trực tuyến!

Ít chơi gôn và định dạng

Mã này giống hệt với phiên bản chơi gôn, chỉ cần không có mapbí danh và eval()gói để dễ đọc.

(a, d, O, M) => {
  for(N = (A = a.push([, 0])) ** d.length; N--;)
    O =
      a.map(([P, T], i) =>
        S -=
          (
            (g = (n, l) =>
              n ?
                l.map(([t, S], i) => g(n - 1, b = [...l], b[i] = [t - 1, S]))
              :
                m = l.map(([t, S]) => s += t > 0 && S, s = 0) && s > m ? m : s
            )(
              P,
              l[n = 0, i].map(m = ([p, t]) => [t, p + t, n += p])
            ),
            n < T && P + T
          ) + (
            l[i] < 1 ? T / 2 : -m
          ),
        S = 0,
        d.map((x, i) =>
          l[
            (j = N / A ** i % A | 0) < A - 1 && o.push([i, j]),
            j
          ].push(x),
          o = [],
          l = a.map(_ => [])
        )
      ) && S < M ? O : (M = S, o)
  return O
}

Làm sao?

Khởi tạo và vòng lặp chính

Điều đầu tiên chúng ta làm là thêm một sinh vật tấn công giả với sức mạnh không xác định và độ dẻo dai bằng . Các phương thức trả về số điện thoại mới tấn công sinh vật, trong đó có một này.0pushA

A = a.push([, 0])

Chúng tôi sẽ chặn sinh vật giả này thay vì chặn không có sinh vật nào cả. Điều này cho phép một số đơn giản hóa trong mã.

Số lượng kết hợp cho người chơi phòng thủ có thể được biểu thị là , trong đó là số lượng người bảo vệ. Chúng tôi sử dụng một vòng lặp for với bộ đếm để lặp lại tất cả các khả năng này.ADDN

for(N = (A = a.push([, 0])) ** d.length; N--;)

Điểm của lần lặp hiện tại và điểm tối đa được lưu trữ trong và tương ứng. Tương tự như vậy, các giải pháp hiện tại và giải pháp tốt nhất được lưu trữ trong và .SMoO

Vào cuối mỗi lần lặp, chúng tôi kiểm tra xem và có cần được cập nhật hay không.MO

O = (...) && S < M ? O : (M = S, o)

Xây dựng quốc phòng của chúng tôi

Dưới đây là khối mã được thực thi ở đầu mỗi lần lặp. Một biến quan trọng mới có , chứa danh sách các trình chặn cho mỗi kẻ tấn công.l

d.map((x, i) =>              // for each defender x at position i:
  l[                         //   update l[]:
    (j = N / A ** i % A | 0) //     j = index of the attacker that we're going to block
    < A - 1 &&               //     if this is not the 'dummy' creature:
    o.push([i, j]),          //       add the pair [i, j] to the current solution
    j                        //     use j as the actual index to update l[]
  ].push(x),                 //   push x in the list of blockers for this attacker
  o = [],                    //   initialize o to an empty list
  l = a.map(_ => [])         //   initialize l to an array containing as many empty lists
                             //   that there are attackers
)                            // end of map()

Tối ưu hóa cuộc tấn công

Các quyết định của những kẻ tấn công là không tương quan với nhau. Tối ưu toàn cầu cho phía tấn công là tổng tối ưu cục bộ cho mỗi kẻ tấn công.

Chúng tôi lặp lại trên mỗi kẻ tấn công sức mạnh và độ bền ở chỉ số .PTi

a.map(([P, T], i) => ...)

Để tìm ra quyết định tốt nhất cho kẻ tấn công, trước tiên chúng tôi xây dựng một danh sách mới có nguồn gốc từ :l[i]

l[n = 0, i].map(m = ([p, t]) => [t, p + t, n += p])

Mỗi mục trong danh sách này chứa độ dẻo dai của trình chặn, theo sau là điểm số của nó (sức mạnh + độ dẻo dai). Trong khi xây dựng danh sách này, chúng tôi cũng tính là tổng sức mạnh của tất cả các trình chặn. Chúng tôi sẽ sử dụng nó sau để kiểm tra xem kẻ tấn công có bị phá hủy không. (Việc các khoản tiền trung gian được lưu trữ dưới dạng mục nhập thứ 3 cho mỗi trình chặn chỉ là một mẹo chơi gôn để tiết kiệm một vài byte. Mục nhập thứ 3 này không được sử dụng.)n

Bây giờ chúng ta gọi hàm đệ quy với 2 tham số: công suất của kẻ tấn công hiện tại và danh sách được xác định ở trên. Hàm này thử tất cả các cách có thể để gửi các điểm sát thương giữa các trình chặn.gP

(g = (n, l) =>            // n = remaining damage points; l = list of blockers
  n ?                     // if we still have damage points:
    l.map(([t, S], i) =>  //   for each blocker of toughness t and score S at index i:
      g(                  //     do a recursive call:
        n - 1,            //       decrement the number of damage points
        b = [...l],       //       create a new instance b of l
        b[i] = [t - 1, S] //       decrement the toughness of blocker i
      )                   //     end of recursive call
    )                     //   end of map()
  :                       // else:
    m =                   //   update the best score m (the lower, the better):
      l.map(([t, S]) =>   //     for each blocker of toughness t and score S:
        s += t > 0 && S,  //       add S to s if this blocker has survived
        s = 0             //       start with s = 0
      ) &&                //     end of map()
      s > m ? m : s       //     set m = min(m, s)
)                         //

Cập nhật điểm hậu vệ

Sau mỗi lần lặp trên kẻ tấn công, chúng tôi cập nhật điểm của hậu vệ với:

S -= (n < T && P + T) + (l[i] < 1 ? T / 2 : -m)

Phần bên trái trừ điểm của kẻ tấn công nếu nó sống sót. Phần bên phải trừ đi một nửa độ dẻo dai của kẻ tấn công nếu cuộc tấn công hoàn toàn không bị chặn, hoặc thêm điểm tấn công tốt nhất nếu không (đó là điểm tồi tệ nhất từ ​​góc độ phòng thủ).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.