import math
import logging
import uuid
import copy
import random
from easydict import EasyDict
from pygame.math import Vector2
from gobigger.utils import format_vector, add_score, Border, deep_merge_dicts
from .base_ball import BaseBall
from .food_ball import FoodBall
from .thorns_ball import ThornsBall
from .spore_ball import SporeBall
[docs]class CloneBall(BaseBall):
"""
Overview:
One of the balls that a single player can control
- characteristic:
* Can move
* Can eat any other ball smaller than itself
* Under the control of the player, the movement can be stopped immediately and contracted towards the center of mass of the player
* Skill 1: Split each unit into two equally
* Skill 2: Spit spores forward
* There is a percentage of weight attenuation, and the radius will shrink as the weight attenuates
"""
[docs] @staticmethod
def default_config():
cfg = BaseBall.default_config()
cfg.update(dict(
acc_weight=100, # Maximum acceleration
vel_max=20, # Maximum velocity
score_init=1, # The initial score of the player's ball
part_num_max=16, # Maximum number of avatars
on_thorns_part_num=10, # Maximum number of splits when encountering thorns
on_thorns_part_score_max=3, # Maximum score of split part when encountering thorns
split_score_min=2.5, # The lower limit of the score of the splittable ball
eject_score_min=2.5, # The lower limit of the score of the ball that can spores
recombine_frame=320, # Time for the split ball to rejoin
split_vel_zero_frame=40, # The time it takes for the speed of the split ball to decay to zero (s)
score_decay_min=2600,
score_decay_rate_per_frame=0.00005, # The score proportion of each state frame attenuation
center_acc_weight=10, # The ratio of actual acceleration to input acceleration
))
return EasyDict(cfg)
def __init__(self, ball_id, position, score, border, team_id, player_id,
vel_given=Vector2(0,0), acc_given=Vector2(0,0),
from_split=False, from_thorns=False, split_direction=Vector2(0,0),
spore_settings=SporeBall.default_config(), sequence_generator=None, **kwargs):
# init other kwargs
kwargs = EasyDict(kwargs)
cfg = CloneBall.default_config()
cfg = deep_merge_dicts(cfg, kwargs)
super(CloneBall, self).__init__(ball_id, position, score, border, **cfg)
self.acc_weight = cfg.acc_weight
self.vel_max = cfg.vel_max
self.score_init = cfg.score_init
self.part_num_max = cfg.part_num_max
self.on_thorns_part_num = cfg.on_thorns_part_num
self.on_thorns_part_score_max = cfg.on_thorns_part_score_max
self.split_score_min = cfg.split_score_min
self.eject_score_min = cfg.eject_score_min
self.recombine_frame = cfg.recombine_frame
self.split_vel_zero_frame = cfg.split_vel_zero_frame
self.score_decay_min = cfg.score_decay_min
self.score_decay_rate_per_frame = cfg.score_decay_rate_per_frame
self.center_acc_weight = cfg.center_acc_weight
self.spore_settings = spore_settings
self.sequence_generator = sequence_generator
self.cfg = cfg
# normal kwargs
self.team_id = team_id
self.player_id = player_id
self.vel_given = vel_given
self.acc_given = acc_given
if from_split:
self.vel_split = self.cal_split_vel_init_from_split(self.radius) * split_direction
elif from_thorns:
self.vel_split = self.cal_split_vel_init_from_thorns(self.radius) * split_direction
else:
self.vel_split = Vector2(0, 0)
self.vel_split_piece = self.vel_split / self.split_vel_zero_frame
self.split_frame = 0
self.frame_since_last_split = 0 # The time of the current ball from the last split
self.vel = self.vel_given + self.vel_split
self.update_direction()
self.check_border()
def update_direction(self):
if self.vel.length() != 0:
self.direction = copy.deepcopy(self.vel.normalize())
else:
self.direction = Vector2(random.random(), random.random()).normalize()
def cal_vel_max(self, radius, ratio):
# return self.vel_max*1/(radius+10) * ratio
return (2.35 + 5.66 / radius) * ratio
def cal_split_vel_init_from_split(self, radius):
return (4.75 + 0.95 * radius) / (self.split_vel_zero_frame / 20) * 2
def cal_split_vel_init_from_thorns(self, radius):
return (13.0 - radius) / (self.split_vel_zero_frame / 20) * 2
[docs] def move(self, given_acc=None, given_acc_center=None, duration=0.05):
"""
Overview:
Realize the movement of the ball, pass in the direction and time parameters
"""
# update acc
if given_acc is not None:
if given_acc.length != 0:
given_acc = given_acc if given_acc.length() < 1 else given_acc.normalize()
self.acc_given = given_acc * self.acc_weight
else:
given_acc = self.acc_given / self.acc_weight
if given_acc_center is not None:
given_acc_center = given_acc_center / self.radius
if given_acc_center.length() != 0 and given_acc_center.length() > 1:
given_acc_center = given_acc_center.normalize()
self.acc_given_center = given_acc_center * self.center_acc_weight
else:
given_acc_center = Vector2(0, 0)
self.acc_given_center = Vector2(0, 0)
self.acc_given_total = self.acc_given + self.acc_given_center
vel_max_ratio_given = given_acc.length()
vel_max_ratio_center = given_acc_center.length()
vel_max_ratio = max(vel_max_ratio_given, vel_max_ratio_center)
# update vel_split
if self.split_frame < self.split_vel_zero_frame:
self.vel_split -= self.vel_split_piece
self.split_frame += 1
else:
self.vel_split = Vector2(0,0)
# update vel_given
self.vel_given = self.vel_given + self.acc_given_total * duration
self.vel_max_ball = self.cal_vel_max(self.radius, ratio=vel_max_ratio)
self.vel_given = format_vector(self.vel_given, self.vel_max_ball)
# udpate vel
self.vel = self.vel_given + self.vel_split
# print(self.vel_split, self.split_frame, self.vel_split_piece)
# update position
self.position = self.position + self.vel * duration
self.update_direction()
self.frame_since_last_split += 1
self.check_border()
[docs] def eat(self, ball, clone_num=None):
"""
Parameters:
clone_num <int>: The total number of balls for the current player
"""
if isinstance(ball, SporeBall) or isinstance(ball, FoodBall) or isinstance(ball, CloneBall):
self.set_score(add_score(self.score, ball.score))
elif isinstance(ball, ThornsBall):
assert clone_num is not None
self.set_score(add_score(self.score, ball.score))
if clone_num < self.part_num_max:
split_num = min(self.part_num_max - clone_num, self.on_thorns_part_num)
return self.on_thorns(split_num=split_num)
else:
logging.debug('CloneBall can not eat {}'.format(type(ball)))
self.check_border()
return True
[docs] def on_thorns(self, split_num) -> list:
'''
Overview:
Split after encountering thorns, calculate the score, position, speed, acceleration of each ball after splitting
Parameters:
split_num <int>: Number of splits added
Returns:
Return a list that contains the newly added balls after the split, the distribution of the split balls is a circle and the center of the circle has a ball
'''
# middle ball
around_score = min(self.score / (split_num + 1), self.on_thorns_part_score_max)
around_radius = self.score_to_radius(around_score)
middle_score = self.score - around_score * split_num
self.set_score(middle_score)
around_positions = []
around_split_directions = []
for i in range(split_num):
angle = 2*math.pi*(i+1) / split_num
unit_x = math.cos(angle)
unit_y = math.sin(angle)
split_direction = Vector2(unit_x, unit_y)
around_position = self.position + Vector2((self.radius+around_radius)*unit_x, (self.radius+around_radius)*unit_y)
around_positions.append(around_position)
around_split_directions.append(split_direction)
balls = []
for p, s in zip(around_positions, around_split_directions):
ball_id = uuid.uuid1() if self.sequence_generator is None else self.sequence_generator.get()
around_ball = CloneBall(ball_id=ball_id, position=p, score=around_score, border=self.border,
team_id=self.team_id, player_id=self.player_id,
vel_given=copy.deepcopy(self.vel_given), acc_given=copy.deepcopy(self.acc_given),
from_split=False, from_thorns=True, split_direction=s, spore_settings=self.spore_settings,
sequence_generator=self.sequence_generator, **self.cfg)
balls.append(around_ball)
return balls
[docs] def eject(self, direction=None) -> list:
'''
Overview:
When spit out spores, the spores spit out must be in the moving direction of the ball, and the position is tangent to the original ball after spitting out
Returns:
Return a list containing the spores spit out
'''
if direction is None or direction.length() == 0:
direction = self.direction
else:
direction = direction.normalize()
if self.score >= self.eject_score_min:
spore_score = self.spore_settings.score_init
self.set_score(self.score - spore_score)
spore_radius = self.score_to_radius(spore_score)
position = self.position + direction * (self.radius + spore_radius)
return SporeBall(ball_id=uuid.uuid1(), position=position, border=self.border, score=spore_score,
direction=direction, owner=self.player_id, **self.spore_settings)
else:
return False
[docs] def split(self, clone_num, direction=None) -> list:
'''
Overview:
Active splitting, the two balls produced by splitting have the same volume, and their positions are tangent to the forward direction
Parameters:
clone_num <int>: The total number of balls for the current player
Returns:
The return value is the new ball after the split
'''
if direction is None or direction.length() == 0:
direction = self.direction
else:
direction = direction.normalize()
if self.score >= self.split_score_min and clone_num < self.part_num_max:
split_score = self.score / 2
self.set_score(split_score)
clone_num += 1
position = self.position + direction * (self.radius * 2)
ball_id = uuid.uuid1() if self.sequence_generator is None else self.sequence_generator.get()
return CloneBall(ball_id=ball_id, position=position, score=self.score, border=self.border,
team_id=self.team_id, player_id=self.player_id,
vel_given=copy.deepcopy(self.vel_given), acc_given=copy.deepcopy(self.acc_given),
from_split=True, from_thorns=False, split_direction=direction, spore_settings=self.spore_settings,
sequence_generator=self.sequence_generator, **self.cfg)
else:
return False
[docs] def rigid_collision(self, ball):
'''
Overview:
When two balls collide, We need to determine whether the two balls belong to the same player
A. If not, do nothing until one party is eaten at the end
B. If the two balls are the same owner, judge whether the age of the two is full or not meet the fusion condition, if they are satisfied, do nothing.
C. If the two balls are the same owner, judge whether the age of the two is full or not meet the fusion condition, Then the two balls will collide with rigid bodies
This function completes the C part: the rigid body collision part, the logic is as follows:
1. To determine the degree of fusion of the two balls, use [the radius of both] and subtract [the distance between the two] as the magnitude of the force
2. Calculate the coefficient according to the weight, the larger the weight, the smaller the coefficient will be
3. Correct the position of the two according to the coefficient and force
Parameters:
ball <CloneBall>: another ball
Returns:
state <bool>: the operation is successful or not
'''
if ball.ball_id == self.ball_id:
return True
assert isinstance(ball, CloneBall), 'ball is not CloneBall but {}'.format(type(ball))
assert self.player_id == ball.player_id
assert self.frame_since_last_split < self.recombine_frame or ball.frame_since_last_split < ball.recombine_frame
p = ball.position - self.position
d = p.length()
if self.radius + ball.radius > d:
f = min(self.radius + ball.radius - d, (self.radius + ball.radius - d) / (d+1e-8))
self.position = self.position - f * p * (ball.score / (self.score + ball.score))
ball.position = ball.position + f * p * (self.score / (self.score + ball.score))
else:
print('WARNINGS: self.radius ({}) + ball.radius ({}) <= d ({})'.format(self.radius, ball.radius, d))
self.check_border()
ball.check_border()
return True
[docs] def judge_rigid(self, ball):
'''
Overview:
Determine whether two balls will collide with a rigid body
Parameters:
ball <CloneBall>: another ball
Returns:
<bool>: collide or not
'''
return self.frame_since_last_split < self.recombine_frame or ball.frame_since_last_split < ball.recombine_frame
[docs] def score_decay(self):
'''
Overview:
Control the score of the ball to decay over time
'''
if self.score > self.score_decay_min:
self.set_score(self.score * (1-self.score_decay_rate_per_frame*math.sqrt(self.radius)))
return True
def flush_frame_since_last_split(self):
self.frame_since_last_split = 0
return True
def __repr__(self) -> str:
return '{}, vel_given={}, acc_given={}, frame_since_last_split={:.3f}, player_id={}, direction={}, team_id={}'\
.format(super().__repr__(), self.vel_given, self.acc_given, self.frame_since_last_split, self.player_id, self.direction, self.team_id)
def save(self):
return [self.position.x, self.position.y, self.radius, self.direction.x, self.direction.y, self.player_id, self.team_id]