Resolve a Super Auto Pets Round

In Super Auto Pets, players build teams of animals that face off in battles to the death unconsciousness. Each animal has an Attack and Health value, and combat occurs in iterations of the following process:

1. The front animals of each team deal damage to each other equal to their Attack at the same time.

2. Both animals lose Health points equal to the damage taken. Health points are never restored in battle.

3. Animals with 0 Health or less faint and are removed from the battle.

As an example consider two teams A and B, which will be represented by a list of tuples (Attack, Health).

A: [(2, 3), (2, 2), (3, 1)] B: [(1, 3), (3, 4), (1, 2)]

To illustrate the battle, the teams will be displayed so that the front animals are facing each other. I will also label each animal for convenience. A battle between these teams would go as follows:

         Pig   Beaver  FishA        Duck   FishB  Otter
Step 0: (3, 1) (2, 2) (2, 3) -> <- (1, 3) (3, 4) (1, 2)
A and B line up opposite each other, with their first animals ready to attack . The arrows show the direction each team is facing.
Step 1: (3, 1) (2, 2) (2, 2)   |   (1, 1) (3, 4) (1, 2)
FishA and Duck attack each other, with FishA dealing 2 damage, and Duck dealing 1. Both are still conscious, so they will attack again next step.
Step 2: (3, 1) (2, 2) (2, 1)   |   (3, 4) (1, 2)
This time FishA leaves Duck with -1 HP, so Duck faints, but FishA survives its attack. Next for B is FishB, but FishA is still alive.
Step 3:        (3, 1) (2, 2)   |   (3, 2) (1, 2)
FishB knocks out FishA and survives FishA's attack with 2 HP. Now Beaver gets to attack
Step 4:               (3, 1)   |   (1, 2)
Beaver and FishB both drop to 0 HP and faint, leaving only Pig and Otter
Step 5:                        |
Otter and Pig knock each other out, leaving no more pets.


Since neither A or B was resilient enough to survive the battle, the result is a draw. If A had any pets left when B's Otter fainted, A would have won (and similar for B in the opposite case).

The Challenge

Your challenge is to write a program or function which takes two teams of pets and outputs the result of their battle. The teams may be empty, and do not have to have the same number of pets.

Your input format may consist of any structure(s) describing the order of each team and the Health and Attack of each animal in that order. Above I have used a list of tuples, each list representing a team in order, and each tuple representing one animal. However, you do not have to conform to this format as long as your input contains the necessary data.

Your solution should output one of three distinct values (which you should specify!) corresponding to a win by the first team, a win by the second team, or a draw. To fulfill this, you also must specify which parts of your input correspond to which team

Standard I/O rules apply, and this is Code Golf, so shortest answer wins.

Examples

The examples take input in the first method described, and output either "A wins!", "B wins!", or "Draw!". I have created a reference implementation in Python 3 on TIO.

A: [(1, 3), (3, 1), (3, 2), (2, 2), (6, 5), (5, 7)]
B: [(3, 1), (1, 1), (1, 2), (1, 2), (4, 3)]
Result: A wins!

A: [(2, 2)]
B: [(2, 2)]
Result: Draw!

A: []
B: [(1, 1)]
Result: B wins!

A: [(1, 3), (3, 1)]
B: [(3, 1), (1, 1), (1, 2), (1, 2), (4, 3)]
Result: B wins!

• Can pets have non-positive Attack? Mar 25 at 9:17
• @UnrelatedString Good question. No, all pets' stats will be positive and nonzero Mar 25 at 15:38

Charcoal, 56 bytes

ＵＭθ⮌ιＷ⌊θ«≔Ｅθ⊟κιＵＭιＥκ⎇ν⁻μ§§ι¬λ⁰μＦ²¿›⌊§ικ⁰⊞§θκ§ικ»Ｉ↨Ｅθ¬ι±¹


Try it online! Link is to verbose version of code. Takes input as a pair of lists of tuples and outputs 1 if A wins, -1 if B wins and 0 for a draw. Explanation:

ＵＭθ⮌ι


Reverse each list so that the front animals are now at the end of the lists.

Ｗ⌊θ«


Repeat while both teams have animals.

≔Ｅθ⊟κι


Pop the front animals from both of the lists.

ＵＭιＥκ⎇ν⁻μ§§ι¬λ⁰μ


Perform an attack.

Ｆ²¿›⌊§ικ⁰⊞§θκ§ικ


Push those animals that still have positive health back to their team.

»Ｉ↨Ｅθ¬ι±¹


Output the final result.

Jelly, 28 bytes

z0_ṚŻ€$Ṗ€Ɗ€1¦»0ZȦƇ€µƬẈ€ẠÞḢIṠ  A monadic Link that accepts a list of the teams as lists of animals each of which is a list of positive integers, [attack, health], and yields [-1] if the first team wins, [0] if there is a draw, or [1] if the second team wins. Try it online! How? z0_ṚŻ€$Ṗ€Ɗ€1¦»0ZȦƇ€µƬẈ€ẠÞḢIṠ - Link: teams
µƬ        - collect up input while distinct, repeatedly applying:
z0                           -   transpose the teams with filler 0 (pair up animals)
Ɗ€1¦                -   apply to the first pair of animals:
$- last two links as a monad: Ṛ - reverse Ż€ - prefix each with a zero _ - animals subtract that (vectorises) Ṗ€ - remove the last (third) value from each »0 - max with zero (vectorises) Z - transpose back to teams ȦƇ€ - for each team keep those without zeros Ẉ€ - length of each for each collected input ẠÞ - sort by: all? Ḣ - head I - forward differences Ṡ - sign (vectorises)  Perl 5, 112 bytes sub{($A,$B)=@_;while(@$A&&@$B){map$_[$_][0][1]-=$_[1-$_][0][0],0,1;map$$_[0][1]<1&&shift@$_,@_}@$A?A:@$B?B:Draw}


Try it online!

05AB1E, 32 bytes

[D€gW_#\Â‚εнUćXR0Dǝ-W1@išë\].S


Assumes the attacks of animals are always positive.
Input as a pair of lists of pairs, where the first inner pairs are the attacking animals. Outputs -1 if team A wins; 0 if it's a draw; and 1 if team B wins.

Explanation:

[            # Loop indefinitely:
D           #  Duplicate the current pair of lists
#  (which is the implicit input in the first iteration)
€g         #  Pop the copy, and get the length of each inner list
W        #  Push the minimum length (without popping)
_       #  If this smallest length is 0 (so a team is without animals):
#      #   Stop the infinite loop
\        #  If not, discard the list of lengths
Â           #  Bifurcate; short for Duplicate & Reverse copy
‚          #  Pair the two together
ε         #  Map over each pair of lists:
        #   Pop and push the two lists separated to the stack
н       #   Pop and push the first pair of the top lists,
#   which is the attacking animal
U      #   Pop and store this pair in variable X
ć       #   Extract head on the other list; push remainder-list and first
#   item separated to the stack, which is the animal that's being
#   attacked
X      #   Push the attacking animal-pair from variable X
R     #   Reverse its [ATK,HP] to [HP,ATK]
0Dǝ  #   Replace its HP (at index 0) with 0: [0,ATK]
- #   Subtract this from the animal that's being attacked
W       #   Push the minimum of this animal (without popping)
1@i    #   If it's >=1 (so the animal is still conscious):
š   #    Prepend it back to the list
ë    #   Else:
\   #    Discard the unconscious animal from the stack
]            # Close the if-statement and loop
# Pop and push the lists separated to the stack
.S         # Compare the two lists (-1 if a<b; 0 if a==b; 1 if a>b)
# (which is output implicitly as result)

• Does âg_ work instead of €gW_?
– Neil
Mar 25 at 15:49
• @Neil I think you meant ø instead of â? And although that does work as check whether an inner list is empty or not, I also use the list of lengths after the infinite loop for the .S compare. With your suggestion I could use øg_# instead of €gW_#\  and then add €g after the ], but it would end up at 32 bytes as well. Mar 25 at 17:44
• I did mean â, although ø is probably better anyway, but I didn't realise you reused the list of lengths.
– Neil
Mar 25 at 19:36
• @Neil The cartesian builtin doesn't work with a pair of lists though. I would need a leading  to throw the pair of lists separated onto the stack first before using the cartesian power builtin in that case. But the zip that ignores trailing items for rows of unequal length does something similar as what you intended to use the cartesian builtin for in this case: checking if either inner list is empty. Mar 25 at 19:40

Desmos, 124 123 bytes

a->f(a,b),b->f(b,a)
a=\ans_1
b=\ans_2
o=sign(a.length-b.length)
k=B[1].x
f(A,B)=\join(\{A[1].y>k:[A[1]-(0,k)],[]\},A[2...])


Exits upon error. Output is o, with its value being -1 if B wins, 0 if tie, and 1 if A wins.

Input is two lists of points. The graphs will further specify how to input the lists.

I had a lot of fun solving this in Desmos! Actions are super fun to mess around with, and this was the perfect challenge to showcase that.

Try It On Desmos!

Try It On Desmos! - Prettified

Explanation

I'm going to explain the expressions a little out of order so that it makes more sense. I don't know why I didn't order it properly initially but whatever.

a=\ans_1
b=\ans_2


Takes in input.

k=B[1].x
f(A,B)=\join(\{A[1].y>k:[A[1]-(0,k)],[]\},A[2...])


This function essentially simulates one "half-turn" of the game. By "half-turn", I mean one of the teams attacking the other. In this case, the inputs are the lists A and B representing the teams of the same name, and it will simulate team B attacking team A.

k=B[1].x is the attack of the first animal on team B.

\{A[1].y>k:[A[1]-(0,k)],[]\} basically means: if the health of the first animal on team A (A[1].y) is greater than k, then return the list [A[1]-(0,k)], which simulates team A's first animal taking k damage. If A[1].y>k is false, then that means the first animal on team A does not survive the hit, so we return an empty list to represent that. You might be wondering why we have to return a list for [A[1]-(0,k)] instead of a number, since you can join a number with a list. This is because of Desmos's underlying vectorization for almost anything that you do with lists. In this case, if I had just returned A[1]-(0,k) instead of [A[1]-(0,k)], then Desmos will vectorize the entire piecewise expression based on the empty list, which will, in turn, always make the piecewise expression equal to an empty list. By making the number into a list, we avoid this issue completely and it behaves as expected.

f(A,B)=\join( . . . ,A[2...]): The newly calculated stats of the first animal of team A is then prepended with the rest of A. The function f(A,B) returns this updated list.

a->f(a,b),b->f(b,a)


The ticker runs these actions continually, which simulates the game, updating the input lists in place.

a->f(a,b): Team b attacks team a and updates list a after the attack.

b->f(b,a): Team a attacks team b and updates list b. Conveniently, the value of a used in this action is actually the old value of a before the attack on team a (a->f(a,b)) occurred, which essentially simulates the two teams attacking each other at the same time. If it used the newly updated a, then it would be harder to simulate the attacks as you would most likely need to keep track of list a before the attack, so that you can accurately apply the attack on team b (or else it would be team b attacks team a, then team a attacks team b, instead of attacking at the same time).

Once at least one of the lists becomes empty, then the ticker errors out, because when calling the function f(A,B), either A[1] or B[1] would be undefined because one of them would be empty, which will cause A[1].y or B[1].x to throw an error. When the ticker throws an error, we will know that the game is over, since the error indicates that one or both teams have no more animals.

o=sign(a.length-b.length)


Returns the final output. If a.length > b.length (indicates that a wins), then a.length-b.length will be positive, so o=1. If a.length < b.length (indicates that b wins), then a.length-b.length will be negative, so o=-1. If a.length = b.length (indicates that it was a tie), then a.length-b.length will be 0, so o=0`.