# Filename: A15c_2D_perfect_kiss_serverN.py
# Written by: James D. Miller
# 5:30 PM Sat May 18, 2019

import sys, os
import pygame
import datetime
import math
import random
import time

import commands, platform

import inspect

# PyGame Constants
from pygame.locals import *
from pygame.color import THECOLORS

# PyGame gui
from pgu import gui

# Import the vector class from a local module (in this same directory)
from vec2d_jdm import Vec2D

# Networking
from PodSixNet.Server import Server
from PodSixNet.Channel import Channel
import socket

# Argument parsing...
import argparse

#=====================================================================
# Classes
#=====================================================================

class ClientChannel(Channel):
    def __init__(self, *args, **kwargs):
        Channel.__init__(self, *args, **kwargs)
    
    # def Network(self, data):
        # #print "Client State Dictionary:", data
        # #print "Network, data['ID']", data['ID']
        # pass
    
    def Network_CN(self, data):
        #global env
        
        # Store incoming data in the client objects.
        speaking_client_name = 'C' + str(data['ID'])
        
        # Check to make sure that this client is still in the client dictionary.
        if speaking_client_name in env.clients:
            # Mouse controls.
        
            env.clients[speaking_client_name].cursor_location_px = data['mXY']  # mouse x,y
            env.clients[speaking_client_name].buttonIsStillDown = data['mBd']   # mouse button down (true/false)
            env.clients[speaking_client_name].mouse_button = data['mB']         # mouse button number (1,2,3,0)
            
            # Jet controls.
            
            # Make the s key behave as a toggle.
            # If key is up, make it ready to accept the down ('D') event.
            if (data['s'] == 'U'):
                env.clients[speaking_client_name].key_s_onoff = 'ON'
                env.clients[speaking_client_name].key_s = data['s']
            # If getting 'D' from network client and the key is enabled.
            elif (env.clients[speaking_client_name].key_s_onoff == 'ON'):
                env.clients[speaking_client_name].key_s = data['s']
                
            env.clients[speaking_client_name].key_a = data['a']
            env.clients[speaking_client_name].key_d = data['d']
            env.clients[speaking_client_name].key_w = data['w']
            
            # Control for stopping all objects (f for freeze).
            env.clients[speaking_client_name].key_f = data['f']
            
            # Gun controls.
            
            # Make the k key behave as a toggle.
            # If key is up, make it ready to accept the down ('D') event.
            if (data['k'] == 'U'):
                env.clients[speaking_client_name].key_k_onoff = 'ON'
                env.clients[speaking_client_name].key_k = data['k']
            # If getting 'D' from network client and the key is enabled.
            elif (env.clients[speaking_client_name].key_k_onoff == 'ON'):
                env.clients[speaking_client_name].key_k = data['k']
            
            env.clients[speaking_client_name].key_j = data['j']
            env.clients[speaking_client_name].key_l = data['l']
            env.clients[speaking_client_name].key_i = data['i']
            env.clients[speaking_client_name].key_space = data[' ']
            
            # Keep track of client activity...
            env.clients[speaking_client_name].sendCount += 1
            
    def Close(self):
        print "A network client game pad has been closed."
     
     
class GameServer(Server):
    channelClass = ClientChannel
    
    def __init__(self, *args, **kwargs):
        Server.__init__(self, *args, **kwargs)
        self.client_count = 0
        
    # This runs when each client connects.
    def Connected(self, channel, addr):
        #print 'new connection (channel, addr):', channel, addr
        
        self.client_count += 1
        
        if (self.client_count <= 10):
            channel.Send({"action": "hello", "P_ID":self.client_count})
            client_name = 'C' + str(self.client_count)
            # Make a client and put it in the clients list.
            env.clients[client_name] = Client(env.client_colors[client_name])
            # Add the channel as an attribute of the client. Use this to Send to this client.
            env.clients[client_name].channel = channel
        else:
            channel.Send({"action": "hello", "P_ID":0})

        print "self.client_count =", self.client_count

        
class Client:
    def __init__(self, cursor_color):
        self.cursor_location_px = (0,0)   # x_px, y_px
        self.mouse_button = 1             # 1, 2, or 3
        self.buttonIsStillDown = False
        
        self.channel = 0
        # Jet
        self.key_a = "U"
        self.key_s = "U"
        self.key_s_onoff = "ON"
        self.key_d = "U"
        self.key_w = "U"
        
        # Gun
        self.key_j = "U"
        self.key_k = "U"
        self.key_k_onoff = "ON"
        self.key_l = "U"
        self.key_i = "U"
        self.key_space = "U"
        
        # Freeze it
        self.key_f = "U"
        
        # Zoom
        self.key_b = "U"
        self.key_n = "U"
        self.key_m = "U"
        self.key_h = "U"
        self.key_lctrl = 'U'
        
        self.selected_puck = None
        self.cursor_color = cursor_color
        self.bullet_hit_count = 0
        self.bullet_hit_limit = 50.0
        
        self.previousSendCount = 0
        self.sendCount = 0
        self.active = False
        
        # Define the nature of the cursor strings, one for each mouse button.
        self.mouse_strings = {'string1':{'c_drag':   2.0, 'k_Npm':   60.0},
                              'string2':{'c_drag':   0.2, 'k_Npm':    2.0},
                              'string3':{'c_drag':  20.0, 'k_Npm': 1000.0}}
                                        
    def calc_string_forces_on_pucks(self):
        # Calculated the string forces on the selected puck and add to the aggregate
        # that is stored in the puck object.
        
        # Only check for a selected puck if one isn't already selected. This keeps
        # the puck from unselecting if cursor is dragged off the puck!
        if (self.selected_puck == None):
            if self.buttonIsStillDown:
                self.selected_puck = air_table.checkForPuckAtThisPosition(self.cursor_location_px)        
        
        else:
            if not self.buttonIsStillDown:
                # Unselect the puck and bomb out of here.
                self.selected_puck.selected = False
                self.selected_puck = None
                return None
            
            # Use dx difference to calculate the hooks law force being applied by the tether line. 
            # If you release the mouse button after a drag it will fling the puck.
            # This tether force will diminish as the puck gets closer to the mouse point.
            dx_2d_m = env.ConvertScreenToWorld(Vec2D(self.cursor_location_px)) - self.selected_puck.pos_2d_m
            
            stringName = "string" + str(self.mouse_button)
            self.selected_puck.cursorString_spring_force_2d_N   += dx_2d_m * self.mouse_strings[stringName]['k_Npm']
            self.selected_puck.cursorString_puckDrag_force_2d_N += (self.selected_puck.vel_2d_mps * 
                                                                    -1 * self.mouse_strings[stringName]['c_drag'])
            
    def draw_cursor_string(self):
        line_points = [env.ConvertWorldToScreen(self.selected_puck.pos_2d_m), self.cursor_location_px]
        if (self.selected_puck != None):
            pygame.draw.line(game_window.surface, self.cursor_color, line_points[0], line_points[1], 1)
                    
    def draw_fancy_server_cursor(self):
        self.draw_server_cursor( self.cursor_color, 0)
        self.draw_server_cursor( THECOLORS["black"], 1)

    def draw_server_cursor(self, color, edge_px):
        cursor_outline_vertices = []
        cursor_outline_vertices.append(  self.cursor_location_px )
        cursor_outline_vertices.append( (self.cursor_location_px[0] + 10,  self.cursor_location_px[1] + 10) )
        cursor_outline_vertices.append( (self.cursor_location_px[0] +  0,  self.cursor_location_px[1] + 15) )
        
        pygame.draw.polygon(game_window.surface, color, cursor_outline_vertices, edge_px)

        
class runningAvg:
    def __init__(self, n_target):
        self.n_target = n_target
        self.base = 5
        self.reset()
    
    def update(self, new_value):
        if self.n_in_avg < self.n_target:
            self.total += new_value
            self.n_in_avg += 1
        else:
            # Add the new value and subtract the oldest.
            self.total += new_value - self.values[0]
            # Discard the oldest value.
            self.values.pop(0)
        self.values.append( new_value)
        
        self.rawResult = self.total / float( self.n_in_avg)
        
        # round the raw result to the nearest 5, e.g. 235, 425...
        self.result = int( self.base * round( float( self.rawResult)/self.base))
        return self.result
        
    def reset(self):
        self.n_in_avg = 0
        self.result = 0.0
        self.values = []
        self.total = 0.0
        
        
class Puck:
    def __init__(self, pos_2d_m, radius_m, density_kgpm2, puck_color = THECOLORS["grey"], coef_rest=0.85, CR_fixed=False,
                    vel_2d_mps=Vec2D(0.0,0.0)):
        self.radius_m = radius_m
        self.radius_px = int(round(env.px_from_m(self.radius_m * env.viewZoom)))

        self.density_kgpm2 = density_kgpm2    # mass per unit area
        self.mass_kg = self.density_kgpm2 * math.pi * self.radius_m ** 2
        
        self.coef_rest = coef_rest
        # This parameter inhibits the changing of the puck's CR when gravity is toggled on and off.
        self.CR_fixed = CR_fixed
        
        self.pos_2d_m = pos_2d_m
        self.vel_2d_mps = vel_2d_mps
        
        self.SprDamp_force_2d_N = Vec2D(0.0,0.0)
        self.jet_force_2d_N = Vec2D(0.0,0.0)
        self.cursorString_spring_force_2d_N = Vec2D(0.0,0.0)
        self.cursorString_puckDrag_force_2d_N = Vec2D(0.0,0.0)

        self.impulse_2d_Ns = Vec2D(0.0,0.0)
        
        self.selected = False
        
        self.color = puck_color
        
        self.client_name = None
        self.jet = None
        self.gun = None
        
        self.hit = False
        self.hitflash_duration_timer_s = 0.0
        # Make the hit flash persist for this number of seconds:
        if platform.system() == 'Linux':
            self.hitflash_duration_timer_limit_s = 0.15        
        else:
            self.hitflash_duration_timer_limit_s = 0.05        
        
        # Bullet data...
        self.bullet = False
        self.birth_time_s = env.time_s
        self.age_limit_s = 3.0
        
        
    # If you print an object instance...
    def __str__(self):
        return "puck: x is %s, y is %s" % (self.pos_2d_m.x, self.pos_2d_m.y)
        
    def draw(self, tempColor=None):
        # Convert x,y to pixel screen location and then draw.
        
        self.pos_2d_px = env.ConvertWorldToScreen( self.pos_2d_m)
        #print "draw position", self.pos_px[0], self.pos_px[1]
        
        # Update based on zoom factor
        self.radius_px = int(round(env.px_from_m( self.radius_m)))
        if (self.radius_px < 3):
            self.radius_px = 3
            
        # Just after a hit, fill the whole circle with RED (i.e., thickness = 0).
        if self.hit:
            puck_circle_thickness = 0
            puck_color = THECOLORS["red"]
            self.hitflash_duration_timer_s += dt_render_s
            if self.hitflash_duration_timer_s > self.hitflash_duration_timer_limit_s:
                self.hit = False
        else:
            puck_circle_thickness = 3
            if (tempColor != None):
                puck_color = tempColor
            else:    
                puck_color = self.color
        
        # Draw main puck body. First, check these integers. If too large they can crash the
        # script as the python integers convert to c integers.
        if (abs(self.pos_2d_px[0]) < 1000) and (abs(self.pos_2d_px[1]) < 1000):
            pygame.draw.circle(game_window.surface, puck_color, self.pos_2d_px, self.radius_px, puck_circle_thickness)
        
            # Draw life (poor health) indicator circle.
            if (((self.client_name != None) and env.clients[self.client_name].active) or (self.client_name == 'test')) and (not self.bullet):
                spent_fraction = float(env.clients[self.client_name].bullet_hit_count) / float(env.clients[self.client_name].bullet_hit_limit)
                life_radius = spent_fraction * self.radius_px
                if (life_radius > 2.0):
                    life_radius_px = int(round(life_radius))
                else:
                    life_radius_px = 2
                
                pygame.draw.circle(game_window.surface, THECOLORS["red"], self.pos_2d_px, life_radius_px, 1)

            
class RotatingTube:
    def __init__(self, puck):
        # Associate the tube with the puck.
        self.puck = puck
    
        self.color = env.clients[self.puck.client_name].cursor_color
        
        # Degrees of rotation per second.
        #self.rotation_rate_dps = 360.0
        
        # Scaling factors to manage the aspect ratio of the tube.
        self.sf_x = 0.15
        self.sf_y = 0.50
        
        # Notice the counter-clockwise drawing pattern. Four vertices for a rectangle.
        # Each vertex is represented by a vector.
        self.tube_vertices_2d_m = [Vec2D(-0.50 * self.sf_x, 0.00 * self.sf_y), 
                                   Vec2D( 0.50 * self.sf_x, 0.00 * self.sf_y), 
                                   Vec2D( 0.50 * self.sf_x, 1.00 * self.sf_y),
                                   Vec2D(-0.50 * self.sf_x, 1.00 * self.sf_y)]
        
        # Define a normal (1 meter) pointing vector to keep track of the direction of the jet.
        self.direction_2d_m = Vec2D(0.0, 1.0)
        
    def rotate_vertices(self, vertices_2d_m, angle_deg):
        # Put modified vectors in a new list.
        rotated_vertices_2d_m = []
        for vertex_2d_m in vertices_2d_m:
            rotated_vertices_2d_m.append( vertex_2d_m.rotated( angle_deg))
        return rotated_vertices_2d_m
    
    def rotate_everything(self, angle_deg):
        # Rotate the pointer.
        self.direction_2d_m = self.direction_2d_m.rotated( angle_deg)
        
        # Rotate the tube.
        self.tube_vertices_2d_m = self.rotate_vertices( self.tube_vertices_2d_m, angle_deg)
                    
    def convert_from_world_to_screen(self, vertices_2d_m, base_point_2d_m):
        vertices_2d_px = []
        for vertex_2d_m in vertices_2d_m:
            # Calculate absolute position of this vertex.
            vertices_2d_px.append( env.ConvertWorldToScreen( vertex_2d_m + base_point_2d_m))
        return vertices_2d_px
        
    def draw_tube(self, line_thickness=3):
        # Draw the tube on the game-window surface. Establish the base_point as the center of the puck.
        pygame.draw.polygon(game_window.surface, self.color, 
                            self.convert_from_world_to_screen(self.tube_vertices_2d_m, self.puck.pos_2d_m), line_thickness)


class Jet( RotatingTube):
    def __init__(self, puck):
        RotatingTube.__init__(self, puck)
        
        # Degrees of rotation per second.
        self.rotation_rate_dps = 360.0
        
        self.color = THECOLORS["yellow"]
        
        # The jet flame (triangle)
        self.flame_vertices_2d_m =[Vec2D(-0.50 * self.sf_x, 1.02 * self.sf_y), 
                                   Vec2D( 0.50 * self.sf_x, 1.02 * self.sf_y), 
                                   Vec2D(-0.00 * self.sf_x, 1.80 * self.sf_y)]
                                   
        # Scaler magnitude of jet force.
        self.jet_force_N = 1.3 * self.puck.mass_kg * abs(air_table.gON_2d_mps2.y)
        
        # Point everything down for starters.
        self.rotate_everything( 180)
        
    def turn_jet_forces_onoff(self, client_name):
        if (env.clients[client_name].key_w == "D"):
            # Force on puck is in the opposite direction of the jet tube.
            self.puck.jet_force_2d_N = self.direction_2d_m * (-1) * self.jet_force_N
        else:    
            self.puck.jet_force_2d_N = self.direction_2d_m * 0.0
            
    def client_rotation_control(self, client_name):
        if (env.clients[client_name].key_a == "D"):
            self.rotate_everything( +1 * self.rotation_rate_dps * dt_render_s)
        if (env.clients[client_name].key_d == "D"):
            self.rotate_everything( -1 * self.rotation_rate_dps * dt_render_s)
        if (env.clients[client_name].key_s == "D"):
            # Rotate jet tube to be in the same direction as the motion of the puck.
            puck_velocity_angle = self.puck.vel_2d_mps.get_angle()
            current_jet_angle = self.direction_2d_m.get_angle()
            self.rotate_everything(puck_velocity_angle - current_jet_angle)
            
            #self.rotate_everything(180)
            
            # Reset this so it doesn't keep flipping. Just want it to flip the
            # direction once but not keep flipping.
            # This first line is enough to keep the local client from flipping again because
            # the local keyboard doesn't keep sending the "D" event if the key is held down.
            env.clients[client_name].key_s = "U"
            # This second one is also needed for the network clients because they keep
            # sending the "D" until they release the key.
            env.clients[client_name].key_s_onoff = "OFF"

    
    def rotate_everything(self, angle_deg):
        # Rotate the pointer.
        self.direction_2d_m = self.direction_2d_m.rotated( angle_deg)
        
        # Rotate the tube.
        self.tube_vertices_2d_m = self.rotate_vertices( self.tube_vertices_2d_m, angle_deg)
        
        # Rotate the flame.
        self.flame_vertices_2d_m = self.rotate_vertices( self.flame_vertices_2d_m, angle_deg)

    def draw(self):
        # Draw the jet tube.        
        self.draw_tube()
        
        # Draw the red flame.
        if (env.clients[self.puck.client_name].key_w == "D"):
            pygame.draw.polygon(game_window.surface, THECOLORS["red"], 
                                self.convert_from_world_to_screen(self.flame_vertices_2d_m, self.puck.pos_2d_m), 0)
                                
    
class Gun( RotatingTube):
    def __init__(self, puck):
        RotatingTube.__init__(self, puck)
        
        # Degrees of rotation per second.
        self.rotation_rate_dps = 180.0
        
        self.color = env.clients[self.puck.client_name].cursor_color
        
        # Run this method of the RotationTube class to set the initial angle of each new gun.
        self.rotate_everything( 45)
        
        self.bullet_speed_mps = 5.0
        self.fire_time_s = env.time_s
        self.firing_delay_s = 0.1
        self.bullet_count = 0
        self.bullet_count_limit = 10
        self.gun_recharge_wait_s = 2.5
        self.gun_recharge_start_time_s = env.time_s
        self.gun_recharging = False
        
        self.testing_gun = False
    
        self.shield = False
        self.shield_hit = False
        self.shield_hit_duration_s = 0.0
        # Make the hit remove the shield for this number of seconds:
        self.shield_hit_duration_limit_s = 0.05        
        self.shield_hit_count = 0
        self.shield_hit_count_limit = 20
        self.shield_recharging = False
        self.shield_recharge_wait_s = 4.0
        self.shield_recharge_start_time_s = env.time_s
    
    def client_rotation_control(self, client_name):
        if (env.clients[client_name].key_j == "D"):
            self.rotate_everything( +self.rotation_rate_dps * dt_render_s)
        if (env.clients[client_name].key_l == "D"):
            self.rotate_everything( -self.rotation_rate_dps * dt_render_s)
        if (env.clients[client_name].key_k == "D"):
            # Rotate jet tube to be in the same direction as the motion of the puck.
            puck_velocity_angle = self.puck.vel_2d_mps.get_angle()
            current_gun_angle = self.direction_2d_m.get_angle()
            self.rotate_everything(puck_velocity_angle - current_gun_angle)
            
            # Reset this so it doesn't keep flipping. Just want it to flip the
            # direction once but not keep flipping.
            # This first line is enough to keep the local client from flipping again because
            # the local keyboard doesn't keep sending the "D" event if the key is held down.
            env.clients[client_name].key_k = "U"
            # This second one is also needed for the network clients because they keep
            # sending the "D" until they release the key.
            env.clients[client_name].key_k_onoff = "OFF"
    
    def control_firing(self, client_name):
        # Fire only if the shield is off.
        if ((env.clients[client_name].key_i == "D") and (not self.shield)) or self.testing_gun:
            # Fire the gun.
            if ((env.time_s - self.fire_time_s) > self.firing_delay_s) and (not self.gun_recharging):
                self.fire_gun()
                self.bullet_count += 1
                # Timestamp the firing event.
                self.fire_time_s = env.time_s
        
        # Check to see if gun bullet count indicates the need to start recharging.
        if (self.bullet_count > self.bullet_count_limit):
            self.gun_recharge_start_time_s = env.time_s
            self.gun_recharging = True
            self.bullet_count = 0
        
        # If recharged.
        if (self.gun_recharging and (env.time_s - self.gun_recharge_start_time_s) > self.gun_recharge_wait_s):
            self.gun_recharging = False
                
    def fire_gun(self):
        bullet_radius_m = 0.05
        # Set the initial position of the bullet so that it clears (doesn't collide with) the host puck.
        initial_position_2d_m = (self.puck.pos_2d_m +
                                (self.direction_2d_m * (1.1 * self.puck.radius_m + 1.1 * bullet_radius_m)) )
        temp_bullet = Puck(initial_position_2d_m,  bullet_radius_m, 0.3)
        
        # Relative velocity of the bullet: the bullet velocity as seen from the host puck. This is the
        # speed of the bullet relative to the motion of the host puck (host velocity BEFORE the firing of 
        # the bullet).
        bullet_relative_vel_2d_mps = self.direction_2d_m * self.bullet_speed_mps
        
        # Absolute velocity of the bullet.
        temp_bullet.vel_2d_mps = self.puck.vel_2d_mps + bullet_relative_vel_2d_mps
        
        temp_bullet.bullet = True
        temp_bullet.color = env.clients[self.puck.client_name].cursor_color
        temp_bullet.client_name = self.puck.client_name
        
        air_table.pucks.append( temp_bullet)
        
        # Calculate the recoil impulse from firing the gun (opposite the direction of the bullet).
        self.puck.impulse_2d_Ns = bullet_relative_vel_2d_mps * temp_bullet.mass_kg * (-1)
    
    def control_shield(self, client_name):
        if (env.clients[client_name].key_space == "D") and (not self.shield_recharging):
            self.shield = True
        else:
            self.shield = False
        
        # Check to see if the shield hit count indicates the need to start recharging.
        if (self.shield_hit_count > self.shield_hit_count_limit):
            self.shield_recharge_start_time_s = env.time_s
            self.shield = False
            self.shield_recharging = True
            self.shield_hit_count = 0
        
        # If recharged.
        if (self.shield_recharging and (env.time_s - self.shield_recharge_start_time_s) > self.shield_recharge_wait_s):
            self.shield_recharging = False
    
    def draw(self):
        # Draw the gun tube.
        if (self.gun_recharging):
            line_thickness = 3
        else:
            line_thickness = 0
        
        # Draw the jet tube.
        self.draw_tube( line_thickness)
        
        # Draw the shield.
        if (self.shield):
            if self.shield_hit:
                # Don't draw the shield for a moment after the hit. This visualizes the shield hit.
                self.shield_hit_duration_s += dt_render_s
                if (self.shield_hit_duration_s > self.shield_hit_duration_limit_s):
                    self.shield_hit = False
                    
            else:
                pygame.draw.circle(game_window.surface, self.color, self.puck.pos_2d_px, self.puck.radius_px + 6, 4)

                
class Spring:
    def __init__(self, p1, p2, length_m=3.0, strength_Npm=0.5, spring_color=THECOLORS["yellow"], width_m=0.025, drag_c=0.0):
        
        # Optionally this spring can have one end pinned to a vector point. Do this by passing in p2 as a vector.
        if (p2.__class__.__name__ == 'Vec2D'):
            # Create a point puck at the pinning location.
            # The location of this point puck will never change because
            # it is not in the pucks list that is processed by the
            # physics engine.
            p2 = Puck( p2, 1.0, 1.0)
            p2.vel_2d_mps = Vec2D(0.0,0.0)
            length_m = 0.0
        
        self.p1 = p1
        self.p2 = p2
        self.p1p2_separation_2d_m = Vec2D(0,0)
        self.p1p2_separation_m = 0
        self.p1p2_normalized_2d = Vec2D(0,0)
        
        self.length_m = length_m
        self.strength_Npm = strength_Npm
        self.damper_Ns2pm2 = 0.5 #5.0 #0.05 #0.15
        self.unstretched_width_m = width_m #0.05
        
        self.drag_c = drag_c
        
        self.spring_vertices_2d_m = []
        self.spring_vertices_2d_px = []
        
        self.spring_color = spring_color
        self.draw_as_line = False
    
    def calc_spring_forces_on_pucks(self):
        self.p1p2_separation_2d_m = self.p1.pos_2d_m - self.p2.pos_2d_m
        
        self.p1p2_separation_m =  self.p1p2_separation_2d_m.length()
        
        # The pinned case needs to be able to handle the zero length spring. The 
        # separation distance will be zero when the pinned spring is at rest.
        # This will cause a divide by zero error if not handled here.
        if ((self.p1p2_separation_m == 0.0) and (self.length_m == 0.0)):
            spring_force_on_1_2d_N = Vec2D(0.0,0.0)
        else:
            self.p1p2_normalized_2d = self.p1p2_separation_2d_m / self.p1p2_separation_m
            
            # Spring force:  acts along the separation vector and is proportional to the separation distance.
            spring_force_on_1_2d_N = self.p1p2_normalized_2d * (self.length_m - self.p1p2_separation_m) * self.strength_Npm
        
        # Damper force: acts along the separation vector and is proportional to the relative speed.
        v_relative_2d_mps = self.p1.vel_2d_mps - self.p2.vel_2d_mps
        v_relative_alongNormal_2d_mps = v_relative_2d_mps.projection_onto(self.p1p2_separation_2d_m)
        damper_force_on_1_N = v_relative_alongNormal_2d_mps * self.damper_Ns2pm2
        
        # Net force by both spring and damper
        SprDamp_force_2d_N = spring_force_on_1_2d_N - damper_force_on_1_N
        
        # This force acts in opposite directions for each of the two pucks. Notice the "+=" here, this
        # is an aggregate across all the springs. This aggregate MUST be reset (zeroed) after the movements are
        # calculated. So by the time you've looped through all the springs, you get the NET force, one each ball, 
        # applied of all individual springs.
        self.p1.SprDamp_force_2d_N += SprDamp_force_2d_N * (+1)
        self.p2.SprDamp_force_2d_N += SprDamp_force_2d_N * (-1)
        
        # Add in some drag forces if a non-zero drag coef is specified. These are based on the
        # velocity of the pucks (not relative speed as is the case above for damper forces).
        self.p1.SprDamp_force_2d_N += self.p1.vel_2d_mps * (-1) * self.drag_c
        self.p2.SprDamp_force_2d_N += self.p2.vel_2d_mps * (-1) * self.drag_c

    def width_to_draw_m(self):
        width_m = self.unstretched_width_m * (1 + 0.30 * (self.length_m - self.p1p2_separation_m))
        if width_m < (0.05 * self.unstretched_width_m):
            self.draw_as_line = True
            width_m = 0.0
        else:
            self.draw_as_line = False
        return width_m
    
    def draw(self):
        # Change the width to indicate the stretch or compression in the spring. Note, it's good to 
        # do this outside of the main calc loop (using the rendering timer). No need to do all this each
        # time step.
        
        width_m = self.width_to_draw_m()
        
        # Calculate the four corners of the spring rectangle.
        p1p2_perpendicular_2d = self.p1p2_normalized_2d.rotate90()
        self.spring_vertices_2d_m = []
        self.spring_vertices_2d_m.append(self.p1.pos_2d_m + (p1p2_perpendicular_2d * width_m))
        self.spring_vertices_2d_m.append(self.p1.pos_2d_m - (p1p2_perpendicular_2d * width_m))
        self.spring_vertices_2d_m.append(self.p2.pos_2d_m - (p1p2_perpendicular_2d * width_m))
        self.spring_vertices_2d_m.append(self.p2.pos_2d_m + (p1p2_perpendicular_2d * width_m))
        
        # Transform from world to screen.
        self.spring_vertices_2d_px = []
        for vertice_2d_m in self.spring_vertices_2d_m:
            self.spring_vertices_2d_px.append( env.ConvertWorldToScreen( vertice_2d_m))
        
        # Draw the spring
        if self.draw_as_line == True:
            pygame.draw.aaline(game_window.surface, self.spring_color, env.ConvertWorldToScreen(self.p1.pos_2d_m),
                                                                       env.ConvertWorldToScreen(self.p2.pos_2d_m))
        else:
            pygame.draw.polygon(game_window.surface, self.spring_color, self.spring_vertices_2d_px)
        
        
class AirTable:
    def __init__(self, walls_dic):
        self.gON_2d_mps2 = Vec2D(-0.0, -9.0)
        self.gOFF_2d_mps2 = Vec2D(-0.0, -0.0)
        self.g_2d_mps2 = self.gOFF_2d_mps2
        self.g_ON = False
        
        self.pucks = []
        self.controlled_pucks = []
        self.springs = []
        self.walls = walls_dic
        self.collision_count = 0
        self.count_direction = 1        
        
        # Used for wall collisions.
        self.coef_rest = 1.0
        
        self.color_transfer = False
        
        self.stop_physics = False
        self.tangled = False

        self.perfect_kiss = False
        self.FPS_display = True
                             
    def draw(self):
        #{"L_m":0.0, "R_m":10.0, "B_m":0.0, "T_m":10.0}
        topLeft_2d_px =   env.ConvertWorldToScreen( Vec2D( self.walls['L_m'],        self.walls['T_m']))
        topRight_2d_px =  env.ConvertWorldToScreen( Vec2D( self.walls['R_m']-0.01,   self.walls['T_m']))
        botLeft_2d_px =   env.ConvertWorldToScreen( Vec2D( self.walls['L_m'],        self.walls['B_m']+0.01))
        botRight_2d_px =  env.ConvertWorldToScreen( Vec2D( self.walls['R_m']-0.01,   self.walls['B_m']+0.01))
        
        pygame.draw.line(game_window.surface, THECOLORS["orangered1"], topLeft_2d_px,  topRight_2d_px, 1)
        pygame.draw.line(game_window.surface, THECOLORS["orangered1"], topRight_2d_px, botRight_2d_px, 1)
        pygame.draw.line(game_window.surface, THECOLORS["orangered1"], botRight_2d_px, botLeft_2d_px,  1)
        pygame.draw.line(game_window.surface, THECOLORS["orangered1"], botLeft_2d_px,  topLeft_2d_px,  1)
    
    def checkForPuckAtThisPosition(self, x_px_or_tuple, y_px = None):
        if y_px == None:
            self.x_px = x_px_or_tuple[0]
            self.y_px = x_px_or_tuple[1]
        else:
            self.x_px = x_px_or_tuple
            self.y_px = y_px
        
        test_position_m = env.ConvertScreenToWorld(Vec2D(self.x_px, self.y_px))
        for puck in self.pucks:
            vector_difference_m = test_position_m - puck.pos_2d_m
            # Use squared lengths for speed (avoid square root)
            mag_of_difference_m2 = vector_difference_m.length_squared()
            if mag_of_difference_m2 < puck.radius_m**2:
                puck.selected = True
                return puck
        return None

    def update_PuckSpeedAndPosition(self, puck, dt_s):
        # Net resulting force on the puck.
        puck_forces_2d_N = (self.g_2d_mps2 * puck.mass_kg) + (puck.SprDamp_force_2d_N + 
                                                              puck.jet_force_2d_N +
                                                              puck.cursorString_spring_force_2d_N +
                                                              puck.cursorString_puckDrag_force_2d_N +
                                                              puck.impulse_2d_Ns/dt_s)
        
        # Acceleration from Newton's law.
        acc_2d_mps2 = puck_forces_2d_N / puck.mass_kg
        
        # Acceleration changes the velocity:  dv = a * dt
        # Velocity at the end of the timestep.
        puck.vel_2d_mps = puck.vel_2d_mps + (acc_2d_mps2 * dt_s)
        
        # Calculate the new physical puck position using the average velocity.
        # Velocity changes the position:  dx = v * dt
        puck.pos_2d_m = puck.pos_2d_m + (puck.vel_2d_mps * dt_s)
        
        # Now reset the aggregate forces.
        puck.SprDamp_force_2d_N = Vec2D(0.0,0.0)
        puck.cursorString_spring_force_2d_N = Vec2D(0.0,0.0)
        puck.cursorString_puckDrag_force_2d_N = Vec2D(0.0,0.0)
        puck.impulse_2d_Ns = Vec2D(0.0,0.0)
    
    def time_past_kiss(self, dt_s, puckA, puckB):
        # Determine the time between the kiss point and collision detection event (penetration time).
        
        initial_collision_angle = (puckA.pos_2d_m - puckB.pos_2d_m).get_angle_between(Vec2D(1.0,0.0))
        
        # As seen from B.
        puckA_relvel_2d_mps = puckA.vel_2d_mps - puckB.vel_2d_mps
        
        # Previous position vectors (position 1) of the two pucks
        puckA_1_pos_2d_m = puckA.pos_2d_m - puckA.vel_2d_mps * dt_s
        puckB_1_pos_2d_m = puckB.pos_2d_m - puckB.vel_2d_mps * dt_s
        
        # Position vector 2-prime of PuckA
        puckA_2p_pos_2d_m = puckA_1_pos_2d_m + puckA_relvel_2d_mps * dt_s
        
        # A check to see if the collision angle is the same in the new frame of reference (as seen from B).
        #final_collision_angle = (puckA_2p_pos_2d_m - puckB_1_pos_2d_m).get_angle_between(Vec2D(1.0,0.0))
        #print "collision_angle", initial_collision_angle, final_collision_angle
        
        #print "check =", (puckA_2p_pos_2d_m - puckB_1_pos_2d_m).length()/(puckA.radius_m + puckB.radius_m)
        
        # Prime path vectors
        prime_path_puckA_2d_m = puckA_2p_pos_2d_m - puckA_1_pos_2d_m
        prime_normalized_2d_m = prime_path_puckA_2d_m.normal()
        
        # Vector between the original positions of the two pucks.
        A1_B1_path_2d_m = puckB_1_pos_2d_m - puckA_1_pos_2d_m
        
        # Projection of A1_B1_path_2d_m onto the prime vector.
        A1_B1_projection_2d_m = A1_B1_path_2d_m.projection_onto( prime_path_puckA_2d_m)
        
        # B1 to prime path vector (vector to nearest point on prime path). The difference
        # between the B_1 vector and its projection onto the prime vector.
        B1_to_prime_2d_m = A1_B1_path_2d_m - A1_B1_projection_2d_m
        
        # Distance x (scaler). Distance between near point on prime and the A2K (kiss location of A2).
        x_m = ((puckA.radius_m + puckB.radius_m)**2 - B1_to_prime_2d_m.length_squared())**0.5
        x_2d_m = prime_normalized_2d_m * x_m
        
        # Kiss point vector
        puckA_2_kiss_2d_m = puckA_1_pos_2d_m + A1_B1_projection_2d_m - x_2d_m
        #print "A1_B1_projection_2d_m, x_2d_m =", A1_B1_projection_2d_m, x_2d_m
        
        # Vector between detection and kiss.
        d_2d_m = puckA_2p_pos_2d_m - puckA_2_kiss_2d_m
        #print "puckA_2p_pos_2d_m, puckA_2_kiss_2d_m =", puckA_2p_pos_2d_m, puckA_2_kiss_2d_m
        
        # Time between detection and kiss. Avoid zero in the denominator.
        if puckA_relvel_2d_mps.x > 0:
            time_between_kiss_and_detection_s = d_2d_m.x / puckA_relvel_2d_mps.x
            #print "d_2d_m.x, puckA_relvel_2d_mps.x =", d_2d_m.x, puckA_relvel_2d_mps.x
        else:
            time_between_kiss_and_detection_s = d_2d_m.y / puckA_relvel_2d_mps.y
            #print "d_2d_m.y, puckA_relvel_2d_mps.y =", d_2d_m.y, puckA_relvel_2d_mps.y
            
        return time_between_kiss_and_detection_s
    
    def check_for_collisions(self, dt_s):
        
        self.tangled = False
        
        # Wall collisions
        for i, puck in enumerate(self.pucks):
            if not env.inhibit_wall_collisions:
                if (((puck.pos_2d_m.y - puck.radius_m) < self.walls["B_m"]) or ((puck.pos_2d_m.y + puck.radius_m) > self.walls["T_m"])):
                    
                    if env.correct_for_wall_penetration:
                        if (puck.pos_2d_m.y - puck.radius_m) < self.walls["B_m"]:
                            penetration_y_m = self.walls["B_m"] - (puck.pos_2d_m.y - puck.radius_m)
                            puck.pos_2d_m.y += 2 * penetration_y_m
                    
                        if (puck.pos_2d_m.y + puck.radius_m) > self.walls["T_m"]:
                            penetration_y_m = (puck.pos_2d_m.y + puck.radius_m) - self.walls["T_m"]
                            puck.pos_2d_m.y -= 2 * penetration_y_m
                    
                    puck.vel_2d_mps.y *= -1 * min(self.coef_rest, puck.coef_rest)
                    self.collision_count += 1 * self.count_direction
                
                if (((puck.pos_2d_m.x - puck.radius_m) < self.walls["L_m"]) or ((puck.pos_2d_m.x + puck.radius_m) > self.walls["R_m"])):
                    
                    if env.correct_for_wall_penetration:
                        if (puck.pos_2d_m.x - puck.radius_m) < self.walls["L_m"]:
                            penetration_x_m = self.walls["L_m"] - (puck.pos_2d_m.x - puck.radius_m)
                            puck.pos_2d_m.x += 2 * penetration_x_m
                    
                        if (puck.pos_2d_m.x + puck.radius_m) > self.walls["R_m"]:
                            penetration_x_m = (puck.pos_2d_m.x + puck.radius_m) - self.walls["R_m"]
                            puck.pos_2d_m.x -= 2 * penetration_x_m
                            
                    puck.vel_2d_mps.x *= -1 * min(self.coef_rest, puck.coef_rest)
                    self.collision_count += 1 * self.count_direction
                
            # Collisions with other pucks. 
            for otherpuck in self.pucks[i+1:]:
                
                # Check if the two puck circles are overlapping.
                
                # Parallel to the normal
                puck_to_puck_2d_m = otherpuck.pos_2d_m - puck.pos_2d_m
                # Parallel to the tangent
                tangent_p_to_p_2d_m = Vec2D.rotate90(puck_to_puck_2d_m)
                
                # Separation between the pucks, squared (not a vector).
                p_to_p_m2 = puck_to_puck_2d_m.length_squared()
                
                # The sum of the radii of the two pucks, squared.
                r_plus_r_m2 = (puck.radius_m + otherpuck.radius_m)**2

                # A check for the Jello-madness game. If it's tangled, balls
                # will be close and this will be set to True.
                if (p_to_p_m2 < 1.1 * r_plus_r_m2):
                    self.tangled = True
                                    
                # Keep this collision check fast by avoiding square roots.
                if (p_to_p_m2 < r_plus_r_m2):
                    self.collision_count += 1 * self.count_direction
                    #print "collision_count", self.collision_count
                    
                    # If it's a bullet coming from another client, add to the
                    # hit count for non-bullet client.
                    if (puck.client_name != None) and (otherpuck.client_name != None):
                        if (puck.client_name != otherpuck.client_name): 
                            if (otherpuck.bullet and (not puck.bullet)):
                                if not puck.gun.shield:
                                    env.clients[puck.client_name].bullet_hit_count += 1
                                    puck.hit = True
                                    puck.hitflash_duration_timer_s = 0.0
                                else:
                                    puck.gun.shield_hit = True
                                    puck.gun.shield_hit_duration_s = 0.0
                                    puck.gun.shield_hit_count += 1
                    
                    if self.color_transfer == True:
                        #(puck.color, otherpuck.color) = (otherpuck.color, puck.color)
                        pass
                    
                    # Use the p_to_p vector (between the two colliding pucks) as projection target for 
                    # normal calculation.
                    
                    # Draw the overlapping pucks.
                    puck.draw(THECOLORS["red"]); otherpuck.draw(THECOLORS["red"])
                    
                    # The calculate velocity components along and perpendicular to the normal.
                    puck_normal_2d_mps = puck.vel_2d_mps.projection_onto(puck_to_puck_2d_m)
                    puck_tangent_2d_mps = puck.vel_2d_mps.projection_onto(tangent_p_to_p_2d_m)
                    
                    otherpuck_normal_2d_mps = otherpuck.vel_2d_mps.projection_onto(puck_to_puck_2d_m)
                    otherpuck_tangent_2d_mps = otherpuck.vel_2d_mps.projection_onto(tangent_p_to_p_2d_m)
                    
                    relative_normal_vel_2d_mps = otherpuck_normal_2d_mps - puck_normal_2d_mps
                    
                    if env.correct_for_puck_penetration:
                        # Back out a total of 2x of the penetration along the normal. Back-out amounts for each puck is 
                        # based on the velocity of each puck times 2DT where DT is the time of penetration. DT is calculated
                        # from the relative speed and the penetration distance.
                        
                        relative_normal_spd_mps = relative_normal_vel_2d_mps.length()
                        penetration_m = (puck.radius_m + otherpuck.radius_m) - p_to_p_m2**0.5
                        if (relative_normal_spd_mps > 0.00000):
                            if air_table.perfect_kiss:
                                # Use a special perfect-kiss method to determine the time.
                                penetration_time_s = self.time_past_kiss( dt_s, puck, otherpuck)
                            else:    
                                penetration_time_s = penetration_m / relative_normal_spd_mps
                            #print penetration_time_s, self.time_past_kiss( dt_s, puck, otherpuck)
                            
                            penetration_time_scaler_1 = 1.00  # This can be useful for testing to amplify and see the correction.
                            penetration_time_scaler_2 = 1.00
                            
                            # First, reverse the two pucks, to their collision point, along their incoming trajectory paths.
                            if air_table.perfect_kiss:
                                puck.pos_2d_m = puck.pos_2d_m - (puck.vel_2d_mps * (penetration_time_scaler_1 * penetration_time_s))
                                otherpuck.pos_2d_m = otherpuck.pos_2d_m - (otherpuck.vel_2d_mps * (penetration_time_scaler_1 * penetration_time_s))
                                
                                # Draw the perfect-kissing pucks (you'll only be able to see this in the example run that is started by pressing
                                # the 3 key on the number pad. This is one of the pool-shot examples that inhibits screen clears.
                                puck.draw(THECOLORS["cyan"]); otherpuck.draw(THECOLORS["cyan"])
                            
                            else:    
                                puck.pos_2d_m = puck.pos_2d_m - (puck_normal_2d_mps * (penetration_time_scaler_1 * penetration_time_s))
                                otherpuck.pos_2d_m = otherpuck.pos_2d_m - (otherpuck_normal_2d_mps * (penetration_time_scaler_1 * penetration_time_s))
                            
                            # # Test to see how close we got to the just-touching point. Ratio should be close to 1.0000
                            # test_center_to_center_separation = (puck.pos_2d_m - otherpuck.pos_2d_m).length() / (puck.radius_m + otherpuck.radius_m)
                            # print "ratio of c_to_c at kiss point =", '%.30f' % test_center_to_center_separation
                            
                            if air_table.perfect_kiss:
                                # Recalculate the tangent and normals based on the pucks in the just-touching position.
                                puck_to_puck_2d_m = otherpuck.pos_2d_m - puck.pos_2d_m
                                tangent_p_to_p_2d_m = Vec2D.rotate90(puck_to_puck_2d_m)
                                # The calculate velocity components along and perpendicular to the normal.
                                puck_normal_2d_mps = puck.vel_2d_mps.projection_onto(puck_to_puck_2d_m)
                                puck_tangent_2d_mps = puck.vel_2d_mps.projection_onto(tangent_p_to_p_2d_m)
                                otherpuck_normal_2d_mps = otherpuck.vel_2d_mps.projection_onto(puck_to_puck_2d_m)
                                otherpuck_tangent_2d_mps = otherpuck.vel_2d_mps.projection_onto(tangent_p_to_p_2d_m)
                            
                            # Calculate the velocities along the normal AFTER the collision. Use a CR (coefficient of restitution)
                            # of 1 here to better avoid stickiness.
                            CR_puck = 1
                            puck_normal_AFTER_mps, otherpuck_normal_AFTER_mps = self.AandB_normal_AFTER_2d_mps( puck_normal_2d_mps, puck.mass_kg, otherpuck_normal_2d_mps, otherpuck.mass_kg, CR_puck)
                            
                            # Temp values for puck and otherpuck velocities after the collision.
                            puck_vel_2d_mps = puck_normal_AFTER_mps + puck_tangent_2d_mps
                            otherpuck_vel_2d_mps = otherpuck_normal_AFTER_mps + otherpuck_tangent_2d_mps
                            
                            # Finally, travel another penetration time worth of distance using these AFTER-collision velocities.
                            # This will put the pucks where they should have been at the time of collision detection.
                            if air_table.perfect_kiss:
                                puck.pos_2d_m = puck.pos_2d_m + (puck_vel_2d_mps * (penetration_time_scaler_2 * penetration_time_s))
                                otherpuck.pos_2d_m = otherpuck.pos_2d_m + (otherpuck_vel_2d_mps * (penetration_time_scaler_2 * penetration_time_s))
                            else:
                                puck.pos_2d_m = puck.pos_2d_m + (puck_normal_AFTER_mps * (penetration_time_scaler_2 * penetration_time_s))
                                otherpuck.pos_2d_m = otherpuck.pos_2d_m + (otherpuck_normal_AFTER_mps * (penetration_time_scaler_2 * penetration_time_s))
                            
                            # # Just to check, compare the corrected separation with the detected
                            # # overlap. This should be very close to 1.00000... for non-perfect_kiss correction approach.
                            # corrected_sep_m = (puck.pos_2d_m - otherpuck.pos_2d_m).length() - (puck.radius_m + otherpuck.radius_m)
                            # print "ratio of corrected_sep/penetration =", '%.30f' % (corrected_sep_m/penetration_m)
                            
                        else:
                            pass
                            #print "small relative speed"
                            #self.g_2d_mps2 = self.gOFF_2d_mps2
                            # for puck in self.pucks:
                                # puck.vel_2d_mps = Vec2D(0,0)
                           
                    # Assign the AFTER velocities (using the actual CR here) to the puck for use in the next frame calculation.
                    CR_puck = min(puck.coef_rest, otherpuck.coef_rest)
                    puck_normal_AFTER_mps, otherpuck_normal_AFTER_mps = self.AandB_normal_AFTER_2d_mps( puck_normal_2d_mps, puck.mass_kg, otherpuck_normal_2d_mps, otherpuck.mass_kg, CR_puck)
                    
                    # Now that we're done using the current values, set them to the newly calculated AFTERs.
                    puck_normal_2d_mps, otherpuck_normal_2d_mps = puck_normal_AFTER_mps, otherpuck_normal_AFTER_mps
                                        
                    # Add the components back together to get total velocity vectors for each puck.
                    puck.vel_2d_mps = puck_normal_2d_mps + puck_tangent_2d_mps
                    otherpuck.vel_2d_mps = otherpuck_normal_2d_mps + otherpuck_tangent_2d_mps
    
    def normal_AFTER_2d_mps(self, A_normal_BEFORE_2d_mps, A_mass_kg, B_normal_BEFORE_2d_mps, B_mass_kg, CR_puck):
        # For inputs as defined here, this returns the AFTER normal for the first puck in the inputs. So if B
        # is first, it returns the result for the B puck.
        relative_normal_vel_2d_mps = B_normal_BEFORE_2d_mps - A_normal_BEFORE_2d_mps
        return ( ( (relative_normal_vel_2d_mps * (CR_puck * B_mass_kg)) + 
                   (A_normal_BEFORE_2d_mps * A_mass_kg + B_normal_BEFORE_2d_mps * B_mass_kg) ) /
                   (A_mass_kg + B_mass_kg) ) 
    
    def AandB_normal_AFTER_2d_mps(self, A_normal_BEFORE_2d_mps, A_mass_kg, B_normal_BEFORE_2d_mps, B_mass_kg, CR_puck):
        A = self.normal_AFTER_2d_mps(A_normal_BEFORE_2d_mps, A_mass_kg, B_normal_BEFORE_2d_mps, B_mass_kg, CR_puck)
        # Make use of the symmetry in the physics to calculate the B-puck normal (put the B-puck data in the first inputs).
        B = self.normal_AFTER_2d_mps(B_normal_BEFORE_2d_mps, B_mass_kg, A_normal_BEFORE_2d_mps, A_mass_kg, CR_puck)
        return A, B
    

class Environment:
    def __init__(self, screenSize_px, length_x_m):
        self.screenSize_px = Vec2D(screenSize_px)
        self.viewOffset_2d_px = Vec2D(0,0)
        self.viewCenter_px = Vec2D(0,0)
        self.viewZoom = 1
        self.viewZoom_rate = 0.01
    
        self.px_to_m = length_x_m/float(self.screenSize_px.x)
        self.m_to_px = (float(self.screenSize_px.x)/length_x_m)
        
        self.client_colors = {'C1': THECOLORS["orangered1"],'C2': THECOLORS["tan"],'C3': THECOLORS["cyan"],'C4': THECOLORS["blue"],
                              'C5': THECOLORS["pink"], 'C6': THECOLORS["red"],'C7': THECOLORS["coral"],'C8': THECOLORS["green"],
                              'C9': THECOLORS["grey80"],'C10': THECOLORS["rosybrown3"],'test': THECOLORS["purple"]}
                              
        # Add a local (non-network) client to the client dictionary.
        self.clients = {'local':Client(THECOLORS["green"])}
        self.clients['local'].active = True
        
        # General clock time for determining bullet age.
        self.time_s = 0
        # Timer for the Jello Madness game.
        self.game_time_s = 0
        self.general_timer_s = 0
        self.generalTimer = False
        self.FR_avg = runningAvg(300)
        
        self.loopsSinceLastQuietCheck = 0
        
        self.inhibit_screen_clears = False
        self.inhibit_wall_collisions = False
        self.correct_for_wall_penetration = True
        self.correct_for_puck_penetration = True
        
        self.always_render = False
        self.constant_dt_s = None
        self.timeDirection = 1;
        
    def checkForQuietClients(self):
        self.loopsSinceLastQuietCheck += 1
        if self.loopsSinceLastQuietCheck > 20:
            self.loopsSinceLastQuietCheck = 0
            for clientname in self.clients:
                if clientname != 'local':
                    # Check for the no change case (client is quiet).
                    countChange = self.clients[clientname].sendCount - self.clients[clientname].previousSendCount
                    if countChange == 0:
                        self.clients[clientname].active = False
                    else:
                        self.clients[clientname].active = True
                    # Update the previous value for use in the next comparison.
                    self.clients[clientname].previousSendCount = self.clients[clientname].sendCount
                
    def remove_healthless_clients(self):
        # Make a list of terminal clients.
        #print len(air_table.pucks), len(air_table.controlled_pucks)
        
        spent_client_names = []
        for thisclient_name in self.clients:
            if self.clients[thisclient_name].bullet_hit_count > self.clients[thisclient_name].bullet_hit_limit:
                spent_client_names.append( thisclient_name)
                
                # Send the bad news if one of the network clients has died.
                if (thisclient_name not in ['local','test']):
                    self.clients[thisclient_name].channel.Send({"action": "badhealth", "message":"not good"})
                
                print "\"" + thisclient_name + "\"" + " has been popped. "
                
                # Reset the counter for the local client. That will keep this block from running repeatedly
                # when the local puck gets popped. Have to do this because the local client does not get
                # deleted below. That's so it can continue to receive keyboard and mouse input and reset the game
                # if needed. The local client always lives on even if its puck gets popped.
                if thisclient_name == 'local':
                    self.clients[thisclient_name].bullet_hit_count = 0
                
        pucks_list_copy = air_table.pucks[:]
        for puck in pucks_list_copy:
            if puck.client_name in spent_client_names:
                # Had to put this check in to prevent server crash on simultaneous death bullets between two clients.
                # Don't yet understand why this is necessary.
                if (puck in air_table.controlled_pucks):
                    air_table.controlled_pucks.remove( puck)
                    #print "\"" + puck.client_name + "\"" + " has been removed from the controlled puck list."
                    
                air_table.pucks.remove( puck)
        
        for spent_client in spent_client_names:
            # Remove client from client dictionary
            if (spent_client != 'local'):
                del self.clients[ spent_client]
              
        del pucks_list_copy
    
    # Convert from meters to pixels 
    def px_from_m(self, dx_m):
        return dx_m * self.m_to_px * self.viewZoom
    
    # Convert from pixels to meters
    # Note: still floating values here)
    def m_from_px(self, dx_px):
        return float(dx_px) * self.px_to_m / self.viewZoom
    
    def control_zoom_and_view(self):
        if self.clients['local'].key_h == "D":
            self.viewZoom += self.viewZoom_rate * self.viewZoom
        if self.clients['local'].key_n == "D":
            self.viewZoom -= self.viewZoom_rate * self.viewZoom
    
    def ConvertScreenToWorld(self, point_2d_px):
        #self.viewOffset_2d_px = self.viewCenter_px
        x_m = (                       point_2d_px.x + self.viewOffset_2d_px.x) / (self.m_to_px * self.viewZoom)
        y_m = (self.screenSize_px.y - point_2d_px.y + self.viewOffset_2d_px.y) / (self.m_to_px * self.viewZoom)
        return Vec2D( x_m, y_m)

    def ConvertWorldToScreen(self, point_2d_m):
        """
        Convert from world to screen coordinates (pixels).
        In the class instance, we store a zoom factor, an offset indicating where
        the view extents start at, and the screen size (in pixels).
        """

        # self.viewOffset = self.viewCenter - self.screenSize_px/2
        #self.viewOffset = self.viewCenter_px
        x_px = (point_2d_m.x * self.m_to_px * self.viewZoom) - self.viewOffset_2d_px.x
        y_px = (point_2d_m.y * self.m_to_px * self.viewZoom) - self.viewOffset_2d_px.y
        y_px = self.screenSize_px.y - y_px

        # Return a tuple of integers.
        return Vec2D(x_px, y_px, "int").tuple()
        
    def set_allPucks_elastic(self):
        print "CRs for all pucks have been set for elastic collisions (CR=1)"
        for eachpuck in air_table.pucks:
            eachpuck.coef_rest = 1.0

    # The next two functions give an alternate approach to using a modification key. This could also
    # be done by setting up a user key state for shift and controls keys, and use that to see if
    # the modifier key has been pressed.
    
    def ctrl_key_down(self):
        keys = pygame.key.get_pressed()
        if (keys[pygame.K_LCTRL] or keys[pygame.K_RCTRL]):
            return True
            
    def shift_key_down(self):
        keys = pygame.key.get_pressed()
        if (keys[pygame.K_LSHIFT] or keys[pygame.K_RSHIFT]):
            return True
    
    def get_local_user_input(self):
        local_user = self.clients['local']
        
        # Get all the events since the last call to get(). In other-words, holding a key down
        # will not repeatedly yield a KEYDOWN event.
        for event in pygame.event.get():
            if (event.type == pygame.QUIT): 
                sys.exit()
            elif (event.type == pygame.KEYDOWN):
                if (event.key == K_ESCAPE):
                    sys.exit()
                elif (event.key==K_KP1):            
                    return "1p"
                elif (event.key==K_KP2):            
                    return "2p"
                elif (event.key==K_KP3):            
                    return "3p"
                elif (event.key==K_1): 
                    if self.shift_key_down():
                        return "1p"
                    else:
                        return 1           
                elif (event.key==K_2):  
                    if self.shift_key_down():
                        return "2p"
                    else:
                        return 2
                elif (event.key==K_3):
                    if self.shift_key_down():
                        return "3p"
                    else:
                        return 3           
                elif (event.key==K_4):
                    return 4           
                elif (event.key==K_5):
                    return 5
                elif (event.key==K_6):
                    return 6
                elif (event.key==K_7):
                    return 7
                elif (event.key==K_8):
                    return 8
                elif (event.key==K_9):
                    return 9
                elif (event.key==K_0):
                    return 0
                
                elif (event.key==K_c):
                    # Toggle color option.
                    air_table.color_transfer = not air_table.color_transfer
                
                elif ((event.key==K_e) and (not self.shift_key_down())):
                    self.set_allPucks_elastic()
                        
                elif (event.key==K_f):
                    # Stop all the pucks...
                    for puck in air_table.pucks:
                        puck.vel_2d_mps = Vec2D(0,0)
                    print "all puck speeds set to zero"
                
                elif (event.key==K_r):
                    air_table.count_direction *= -1
                    print ""
                    if self.shift_key_down():
                        env.timeDirection *= -1
                        print "time direction has been reversed"
                    else:
                        # Reverse the velocity of all the pucks...
                        for puck in air_table.pucks:
                            puck.vel_2d_mps = puck.vel_2d_mps * (-1)
                        print "puck velocities have been reversed"
                    print "timeDirection =", env.timeDirection, "count direction =", air_table.count_direction
                
                elif (event.key==K_g):
                    # Toggle the logical flag for g.
                    air_table.g_ON = not air_table.g_ON
                    print "g", air_table.g_ON
                    
                    if air_table.g_ON:
                        air_table.g_2d_mps2 = air_table.gON_2d_mps2
                        print "setting puck CRs = 0.85, if not fixed"
                        for eachpuck in air_table.pucks:
                            if not eachpuck.CR_fixed:
                                eachpuck.coef_rest = 0.85
                    else:
                        air_table.g_2d_mps2 = air_table.gOFF_2d_mps2
                        print "setting puck CRs = 1.00, if not fixed"
                        for eachpuck in air_table.pucks:
                            if not eachpuck.CR_fixed:
                                eachpuck.coef_rest = 1.00
                
                elif (event.key==K_F1):
                    # Toggle FPS display on/off
                    air_table.FPS_display = not air_table.FPS_display
                
                elif (event.key==K_z):
                    print ""
                    env.FR_avg.reset()
                    air_table.perfect_kiss = not air_table.perfect_kiss
                    if (air_table.perfect_kiss):
                        self.set_allPucks_elastic()
                    print "perfect kiss =", air_table.perfect_kiss
                    
                # Jet keys
                elif (event.key==K_a):
                    local_user.key_a = 'D'
                elif (event.key==K_s):
                    local_user.key_s = 'D'
                elif (event.key==K_d):
                    local_user.key_d = 'D'
                elif (event.key==K_w):
                    local_user.key_w = 'D'
                
                # Gun keys
                elif (event.key==K_j):
                    local_user.key_j = 'D'
                elif (event.key==K_k):
                    local_user.key_k = 'D'
                elif (event.key==K_l):
                    local_user.key_l = 'D'
                elif (event.key==K_i):
                    local_user.key_i = 'D'
                elif (event.key==K_SPACE):
                    local_user.key_space = 'D'
                    
                # Zoom keys
                elif (event.key==K_b):
                    local_user.key_b = 'D'
                elif (event.key==K_n):
                    local_user.key_n = 'D'
                elif (event.key==K_m):
                    local_user.key_m = 'D'
                elif (event.key==K_h):
                    local_user.key_h = 'D'
                elif (event.key==K_LCTRL):
                    local_user.key_lctrl = 'D'                    
                
                # Control physics for Jello Madness
                elif ((event.key==K_p) and not self.shift_key_down()):
                    air_table.stop_physics = not air_table.stop_physics
                    if (not air_table.stop_physics):
                        env.game_time_s = 0
                        print "game loop is active again"
                    else:
                        print "game loop is paused"
                
                elif ((event.key==K_p) and     self.shift_key_down()):
                    if (env.constant_dt_s == None):
                        env.constant_dt_s = 1.0/env.FR_avg.result
                        print "physics engine is stepping in equal intervals of 1 /", int( env.FR_avg.result)
                    else:
                        env.constant_dt_s = None
                        print "physics engine steps are floating with the game loop"
                    env.FR_avg.reset()
                
                elif ((event.key==K_e) and (self.shift_key_down())):
                    env.inhibit_screen_clears = not env.inhibit_screen_clears
                    print "inhibit_screen_clears =", env.inhibit_screen_clears
                
                elif (event.key==K_t):
                    self.generalTimer = not self.generalTimer
                    if (self.generalTimer):
                        self.general_timer_s = 0
                        print "timer reset and started"
                    else:
                        print "timer stopped"
                        
                else:
                    # Note: the printing of the return value is restricted to a set of expected values.
                    return "nothing set up for this key"
            
            elif (event.type == pygame.KEYUP):
                # Jet keys
                if   (event.key==K_a):
                    local_user.key_a = 'U'
                elif (event.key==K_s):
                    local_user.key_s = 'U'
                elif (event.key==K_d):
                    local_user.key_d = 'U'
                elif (event.key==K_w):
                    local_user.key_w = 'U'
                
                # Gun keys
                elif (event.key==K_j):
                    local_user.key_j = 'U'
                elif (event.key==K_k):
                    local_user.key_k = 'U'
                elif (event.key==K_l):
                    local_user.key_l = 'U'
                elif (event.key==K_i):
                    local_user.key_i = 'U'
                elif (event.key==K_SPACE):
                    local_user.key_space = 'U'
                    
                # Zoom keys
                elif (event.key==K_b):
                    local_user.key_b = 'U'
                elif (event.key==K_n):
                    local_user.key_n = 'U'
                elif (event.key==K_m):
                    local_user.key_m = 'U'
                elif (event.key==K_h):
                    local_user.key_h = 'U'
                elif (event.key==K_LCTRL):
                    local_user.key_lctrl = 'U'
            
            elif event.type == pygame.MOUSEBUTTONDOWN:
                local_user.buttonIsStillDown = True
            
                (button1, button2, button3) = pygame.mouse.get_pressed()
                if button1:
                    local_user.mouse_button = 1
                elif button2:
                    local_user.mouse_button = 2
                elif button3:
                    local_user.mouse_button = 3
                else:
                    local_user.mouse_button = 0
            
            elif event.type == pygame.MOUSEBUTTONUP:
                local_user.buttonIsStillDown = False
                local_user.mouse_button = 0
                
            elif ((event.type == pygame.MOUSEMOTION) and (local_user.key_lctrl == 'D')):
                #print "in mousemotion block", event.pos, event.rel[0], event.rel[1]
                self.viewOffset_2d_px -= Vec2D(event.rel[0], -event.rel[1])
            
            # In all cases, pass the event to the Gui.
            #app.event(event)
        
        if local_user.buttonIsStillDown:
            # This will select a puck when the puck runs into the cursor of the mouse with it's button still down.
            local_user.cursor_location_px = (mouseX, mouseY) = pygame.mouse.get_pos()

        
class GameWindow:
    def __init__(self, screen_tuple_px, title):
        self.width_px = screen_tuple_px[0]
        self.height_px = screen_tuple_px[1]
        
        # The initial World position vector of the Upper Right corner of the screen.
        # Yes, that's right y_px = 0 for UR.
        self.UR_2d_m = env.ConvertScreenToWorld(Vec2D(self.width_px, 0))
        
        # Create a reference to the display surface object. This is a pygame "surface".
        # Screen dimensions in pixels (tuple)
        self.surface = pygame.display.set_mode(screen_tuple_px)

        self.update_caption(title)
        
        self.surface.fill(THECOLORS["black"])
        pygame.display.update()
        
    def update_caption(self, title):
        pygame.display.set_caption( title)
        self.caption = title
    
    def update(self):
        pygame.display.update()
        
    def clear(self):
        # Useful for shifting between the various demos.
        self.surface.fill(THECOLORS["black"])
        pygame.display.update()

        
#===========================================================
# Functions
#===========================================================

def setup_pool_shot():
    env.always_render = True
    env.constant_dt_s = 1/20.0
    
    env.inhibit_wall_collisions = True
    env.inhibit_screen_clears = True
    
    # Randomize the starting x position of the incoming puck. Elastic pucks make it reversible.
    air_table.pucks.append( Puck(Vec2D(random.random()-0.3, 4.80), 0.45, 0.3, THECOLORS["orange"], coef_rest=1.0, CR_fixed=True, vel_2d_mps=Vec2D(  25.0, 0.0)) )
    air_table.pucks.append( Puck(Vec2D(4.0,                 4.30), 0.45, 0.3,                      coef_rest=1.0, CR_fixed=True, vel_2d_mps=Vec2D(   0.0, 0.0)) )
       
def make_some_pucks(resetmode):
    game_window.update_caption("Air Table V.3: Demo #" + str(resetmode)) 
    env.inhibit_wall_collisions = False
    env.inhibit_screen_clears = False
    env.correct_for_wall_penetration = True
    env.correct_for_puck_penetration = True
    air_table.collision_count = 0
    env.FR_avg.reset()
    
    env.always_render = False
    env.constant_dt_s = None
    air_table.perfect_kiss = False
    
    if resetmode == '1p':
        env.correct_for_puck_penetration = False
        air_table.perfect_kiss = False
        setup_pool_shot()
        
    elif resetmode == '2p':
        env.correct_for_puck_penetration = True
        air_table.perfect_kiss = False
        setup_pool_shot()
        
    elif resetmode == '3p':
        env.correct_for_puck_penetration = True
        air_table.perfect_kiss = True
        setup_pool_shot()
        
    elif resetmode == 1:
        #                                           ,radius,density
        air_table.pucks.append( Puck(Vec2D(2.5, 7.5), 0.25, 0.3, THECOLORS["orange"]))
        air_table.pucks.append( Puck(Vec2D(6.0, 2.5), 0.45, 0.3)) # maybe not.
        air_table.pucks.append( Puck(Vec2D(7.5, 2.5), 0.65, 0.3)) 
        air_table.pucks.append( Puck(Vec2D(2.5, 5.5), 1.65, 0.3))
        air_table.pucks.append( Puck(Vec2D(7.5, 7.5), 0.95, 0.3))
    
    
    elif resetmode == 2:
        spacing_factor = 2.0
        grid_size = 4,2
        for j in range(grid_size[0]):
            for k in range(grid_size[1]):
                if ((j,k) == (1,1)):
                    puck_color_value = THECOLORS["orange"]
                else:
                    puck_color_value = THECOLORS["grey"]
                
                air_table.pucks.append( Puck(Vec2D(spacing_factor*(j+1), spacing_factor*(k+1)), 0.75, 0.3, puck_color=puck_color_value))
    
    
    elif resetmode == 3:
        spacing_factor = 1.5
        grid_size = 5,3
        for j in range(grid_size[0]):
            for k in range(grid_size[1]):
                if ((j,k) == (2,2)):
                    puck_color_value = THECOLORS["orange"]
                else:
                    puck_color_value = THECOLORS["grey"]
                    
                air_table.pucks.append( Puck( Vec2D(spacing_factor*(j+1), spacing_factor*(k+1)), 0.55, 0.3, puck_color=puck_color_value))

    
    elif resetmode == 4:
        spacing_factor = 1.1      
        
        if platform.system() == 'Linux':
            grid_size = 5,4
        else:    
            grid_size = 6,5
        
        for j in range( grid_size[0]):
            for k in range( grid_size[1]):
                if ((j,k) == (2,2)):
                    puck_color_value = THECOLORS["orange"]
                else:
                    puck_color_value = THECOLORS["grey"]
                
                air_table.pucks.append( Puck( Vec2D( spacing_factor*(j+1), spacing_factor*(k+1)), radius_m=0.25, density_kgpm2=1.0, 
                                              puck_color=puck_color_value,
                                              CR_fixed=False, coef_rest=0.9) )
            
    elif resetmode == 5:
        air_table.pucks.append( Puck(Vec2D(2.00, 3.00),  0.4, 0.3) )
        air_table.pucks.append( Puck(Vec2D(3.50, 4.50),  0.4, 0.3) )
        
        # No springs on this one.
        #air_table.pucks.append( Puck(Vec2D(3.50, 7.00),  0.95, 0.3) )
    
        spring_strength_Npm2 = 20.0 #18.0
        spring_length_m = 1.5
        air_table.springs.append( Spring(air_table.pucks[0], air_table.pucks[1], spring_length_m, spring_strength_Npm2, width_m=0.2))
    
    
    elif resetmode == 6:
        if platform.system() == 'Linux':
            density = 2.0
            radius = 0.7
            
            # Lower the CR for these pucks and fix them, using CR_fixed, so when gravity 
            # toggles on/off they stay at these levels.
            coef_rest_puck =  0.50
            
            spring_strength_Npm2 = 300.0
            spring_length_m = 2.5
            spring_width_m = 0.07
            spring_drag = 0.0
            spring_damper = 10.0
        else:
            density = 1.5
            radius = 0.7
            
            coef_rest_puck =  0.70
            
            spring_strength_Npm2 = 400.0
            spring_length_m = 2.5
            spring_width_m = 0.07
            spring_drag = 0.0
            spring_damper = 5.0

        air_table.pucks.append( Puck(Vec2D(2.00, 3.00),  radius, density, coef_rest=coef_rest_puck, CR_fixed=True) )
        air_table.pucks.append( Puck(Vec2D(3.50, 4.50),  radius, density, coef_rest=coef_rest_puck, CR_fixed=True) )
        air_table.pucks.append( Puck(Vec2D(5.00, 3.00),  radius, density, coef_rest=coef_rest_puck, CR_fixed=True) )
        
        # No springs on this one.
        air_table.pucks.append( Puck(Vec2D(3.50, 7.00),  0.95, density, coef_rest=coef_rest_puck, CR_fixed=True) )
        
        air_table.springs.append( Spring(air_table.pucks[0], air_table.pucks[1],
                                         spring_length_m, spring_strength_Npm2, width_m=spring_width_m, drag_c=spring_drag))
        air_table.springs.append( Spring(air_table.pucks[1], air_table.pucks[2],
                                         spring_length_m, spring_strength_Npm2, width_m=spring_width_m, drag_c=spring_drag))
        air_table.springs.append( Spring(air_table.pucks[2], air_table.pucks[0],
                                         spring_length_m, spring_strength_Npm2, width_m=spring_width_m, drag_c=spring_drag))
        
        # Increase the shock-absorber strength for each spring.
        for spring in air_table.springs:                                 
            spring.damper_Ns2pm2 = spring_damper

            
    elif resetmode == 7:
        #env.constant_dt_s = 1.0/200.0 # 1.0/200.0 None
        air_table.collision_checking_enabled = True
        env.game_time_s = 0    
        offset_xy_m = Vec2D(2.5, 2.1) 
        
        if platform.system() == 'Linux':
            spacing_factor = 1.0
            grid_size = 3
            density = 45.0
            cr7 = 0.85
            radius = 0.25
            spring_strength_Npm2 = 800.0 #18.0
            spring_length_m = 1.2
            spring_damper_Ns2pm2 = 5.0
        else:
            spacing_factor = 1.0
            grid_size = 3 
            density = 5.0
            cr7 = 0.85
            radius = 0.25
            spring_strength_Npm2 = 800.0 #18.0
            spring_length_m = 1.1            
            spring_damper_Ns2pm2 = 20.0
        
        diagonal_scaling = 1.41 # 1.41 (for square jello)
        grid = grid_size, grid_size
        
        for j in range(grid[0]):
            for k in range(grid[1]):
                if ((j,k) == (2,2)):
                    air_table.pucks.append( Puck( Vec2D( spacing_factor*(j+1), spacing_factor*(k+1)) + offset_xy_m, radius, density, THECOLORS["orange"], coef_rest=cr7, CR_fixed=True) )
                else:
                    air_table.pucks.append( Puck( Vec2D( spacing_factor*(j+1), spacing_factor*(k+1)) + offset_xy_m, radius, density, coef_rest=cr7, CR_fixed=True) )

        # Horizontal springs (between neighbors)            
        for m in range(grid_size*(grid_size-1)):
            air_table.springs.append( Spring(air_table.pucks[m], air_table.pucks[m+grid_size], spring_length_m, spring_strength_Npm2, spring_color=THECOLORS["blue"]))
        
        # Vertical springs
        for m in range(grid_size-1):
            for n in range(grid_size):
                o_index = m + (n * grid_size)
                #print "index:", m, n, o_index, o_index+1
                air_table.springs.append( Spring(air_table.pucks[o_index], air_table.pucks[o_index+1], spring_length_m, spring_strength_Npm2, spring_color=THECOLORS["blue"]))
        
        # Diagonal springs
        for m in range(0, grid_size-1):
            for n in range(1, grid_size):
                o_index = m + (n * grid_size)
                air_table.springs.append( Spring(air_table.pucks[o_index], air_table.pucks[o_index-(grid_size-1)], spring_length_m * diagonal_scaling, spring_strength_Npm2, spring_color=THECOLORS["yellow"]))
        
        # Diagonal springs (perpendicular to the other diagonals)
        for m in range(0, grid_size-1):
            for n in range(0, grid_size-1):
                o_index = m + (n * grid_size)
                air_table.springs.append( Spring(air_table.pucks[o_index], air_table.pucks[o_index+(grid_size+1)], spring_length_m * diagonal_scaling, spring_strength_Npm2, spring_color=THECOLORS["yellow"]))


        # Set the shock-absorber strength for each spring.
        for spring in air_table.springs:                                
            spring.damper_Ns2pm2 = spring_damper_Ns2pm2   
            
            
    elif resetmode == 8:
        air_table.collision_checking_enabled = True
        

        if platform.system() == 'Linux':
            # for Raspberry Pi
            density = 1.0
            
            #                                           ,radius,density
            air_table.pucks.append( Puck(Vec2D(5.0, 2.5), 0.30, density))
            air_table.pucks.append( Puck(Vec2D(4.0, 2.5), 0.30, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 2.5), 0.65, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 5.0), 0.85, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 7.5), 1.15, density))

            # Make some pinned-spring pucks.
            for m in range(0, 3): 
                pinPoint_2d = Vec2D(2.0 + float(m) * 1.65, 4.0)
                tempPuck = Puck(pinPoint_2d, 0.7, density*5.0,  THECOLORS["orange"])
                air_table.pucks.append( tempPuck)
                air_table.springs.append( Spring(tempPuck, pinPoint_2d, strength_Npm=300.0, width_m=0.02, drag_c = 1.5 + 10.0))
        else:
            density = 0.7
            
            #                                           ,radius,density
            air_table.pucks.append( Puck(Vec2D(5.0, 2.5), 0.15, density))
            air_table.pucks.append( Puck(Vec2D(4.0, 2.5), 0.15, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 2.5), 0.65, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 5.0), 0.85, density))
            air_table.pucks.append( Puck(Vec2D(7.5, 7.5), 1.15, density))

            # Make some pinned-spring pucks.
            for m in range(0, 6): 
                pinPoint_2d = Vec2D(2.0 + float(m) * 0.65, 4.0)
                tempPuck = Puck(pinPoint_2d, 0.25, density,  THECOLORS["orange"])
                air_table.pucks.append( tempPuck)
                air_table.springs.append( Spring(tempPuck, pinPoint_2d, strength_Npm=300.0, width_m=0.02, drag_c=1.5))
        

        # Make user/client controllable pucks
        # for all the clients.
        y_puck_position_m = 1.0
        for client_name in env.clients:
            if env.clients[client_name].active:
                tempPuck = Puck(Vec2D(6.0, y_puck_position_m), 0.45, density)
                # Let the puck reference the jet and the jet reference the puck.
                tempPuck.client_name = client_name
                tempPuck.jet = Jet( tempPuck)
                tempPuck.gun = Gun( tempPuck)

                air_table.pucks.append( tempPuck)
                air_table.controlled_pucks.append( tempPuck)
                y_puck_position_m += 1.2
                
                # Reset the hit counters.
                env.clients[client_name].bullet_hit_count = 0
        
        # Keep gun on in a testing puck...

        if args.testPuck == 'on':
            tempPuck = Puck(Vec2D(6.0, y_puck_position_m), 0.45, density)
            # Let the puck reference the jet and the jet reference the puck.
            tempPuck.client_name = "test"
            env.clients[tempPuck.client_name] = Client(env.client_colors[tempPuck.client_name])
            tempPuck.jet = Jet( tempPuck)
            tempPuck.gun = Gun( tempPuck)
            tempPuck.gun.testing_gun = True
            # The default position at instantiation is 45 degrees counter-clockwise from vertical.
            # The degree value specified here is relative to that +45. Negative values are clockwise.
            tempPuck.gun.rotate_everything( -110)
            air_table.pucks.append( tempPuck)
            air_table.controlled_pucks.append( tempPuck)
        
    
    elif resetmode == 9:
        # Make user/client controllable pucks
        # for all the clients.
        y_puck_position_m = 1.0
        for client_name in env.clients:
            if env.clients[client_name].active:
                tempPuck = Puck(Vec2D(6.0, y_puck_position_m), 0.45, 0.3)
                # Let the puck reference the jet and the jet reference the puck.
                tempPuck.client_name = client_name
                tempPuck.jet = Jet( tempPuck)
                tempPuck.gun = Gun( tempPuck)

                air_table.pucks.append( tempPuck)
                air_table.controlled_pucks.append( tempPuck)
                y_puck_position_m += 1.2
    
    else:
        print "Nothing set up for this key."

        
def display_number(numeric_value, font_object,  mode='FPS'):
    if mode=='FPS':
        fps_value = "%.0f" % numeric_value
        if (env.constant_dt_s != None):
            # Small background rectangle for FPS text (left, top, width, height)
            pygame.draw.rect( game_window.surface, THECOLORS["white"], pygame.Rect(10, 10, 64, 20))
            fps_string = fps_value + " (" + str( int( 1/env.constant_dt_s)) + ")"
        else:
            pygame.draw.rect( game_window.surface, THECOLORS["white"], pygame.Rect(10, 10, 35, 20))
            fps_string = fps_value
        txt_surface = font_object.render( fps_string, True, THECOLORS["black"])
        game_window.surface.blit( txt_surface, [18, 11])
    elif mode=='counter':
        pygame.draw.rect( game_window.surface, THECOLORS["white"], pygame.Rect(10, 40, 40, 20))
        cnt_string = "%.0f" % numeric_value
        txt_surface = font_object.render( cnt_string, True, THECOLORS["black"])
        game_window.surface.blit( txt_surface, [18, 41])
    elif mode=='gameTimer':
        time_string = "%.2f" % numeric_value
        txt_surface = font_object.render( time_string, True, THECOLORS["white"])
        game_window.surface.blit( txt_surface, [600, 11])
    elif mode=='generalTimer':
        time_string = "%.1f" % numeric_value
        txt_surface = font_object.render( time_string, True, THECOLORS["white"])
        game_window.surface.blit( txt_surface, [740, 5])
        
#============================================================
# Main procedural script.
#============================================================

def main():

    # A few globals.
    global env, game_window, air_table, args, dt_render_s, dt_physics_s
    
    # Parse parameters provided in the command line.
    # This description string (and parameter help) gets displayed if help is requested (-h added after the filename).
    parser = argparse.ArgumentParser(description='Please add optional client parameters after the file name. For example: \n' + 
                                                 'A16c_2D_B2D_serverN.py off')
    # An optional positional argument.
    parser.add_argument('testPuck', type=str, nargs='?', default='on', help='Please indicate whether the practice puck should be on or off (default is on).')                              
                                    
    args = parser.parse_args()
    print "testPuck:", args.testPuck
    
    pygame.init()

    myclock = pygame.time.Clock()

    if platform.system() == 'Linux':
        window_dimensions_px = (800, 700)   #window_width_px, window_height_px   (600, 500)
    else:
        window_dimensions_px = (800, 700)   #window_width_px, window_height_px   (800, 700)

    # Create the first user/client and the methods for moving between the screen and the world.
    env = Environment(window_dimensions_px, 10.0) # 10m in along the x axis.

    game_window = GameWindow(window_dimensions_px, 'Air Table Server V.2')

    # Define the Left, Right, Bottom, and Top boundaries of the game window.
    air_table = AirTable({"L_m":0.0, "R_m":game_window.UR_2d_m.x, "B_m":0.0, "T_m":game_window.UR_2d_m.y})

    # Add some pucks to the table.
    demo_mode = 1
    make_some_pucks( demo_mode)

    # Setup network server.
    if platform.system() == 'Linux':
        local_ip = commands.getoutput("hostname -I")
    else:
        local_ip = socket.gethostbyname(socket.gethostname())
    print "Server IP address:", local_ip
    game_server = GameServer(localaddr=(local_ip, 4330))

    # Font object for rendering text onto display surface.
    fnt_FPS          = pygame.font.SysFont("Arial", 14)
    fnt_generalTimer = pygame.font.SysFont("Arial", 25)
    fnt_gameTimer    = pygame.font.SysFont("Arial", 60)
    
    dt_render_s = 0.0
    dt_render_limit_s = 1.0/120.0 # = 1/FR

    # An object containing the running average of the framerate of the physics calculations.
    if platform.system() == 'Linux':
        env.FR_avg.n_target = 50 #50
    else:
        env.FR_avg.n_target = 500 #Default is 300
    
    while True:
        if (env.constant_dt_s != None):
            gameLoop_FR_limit = int(1.0/env.constant_dt_s)
        else:
            gameLoop_FR_limit = 480 # default
            
        dt_gameLoop_s = float( myclock.tick( gameLoop_FR_limit) * 1e-3)
        
        if (env.constant_dt_s != None):
            dt_physics_s = env.constant_dt_s
        else:
            dt_physics_s = dt_gameLoop_s
                
        if air_table.FPS_display:
            env.FR_avg.update( 1/dt_gameLoop_s)
        
        # Get input from local user.
        resetmode = env.get_local_user_input()
        
        # This dt check avoids problem when dragging the game window.
        if ( ((dt_gameLoop_s < 0.10) and (not air_table.stop_physics)) or env.always_render):
            
            ok_to_render = (dt_render_s > dt_render_limit_s) or env.always_render
            
            # Reset the game based on local user control.
            if resetmode in ["1p","2p","3p",1,2,3,4,5,6,7,8,9,0]:
                demo_mode = resetmode
                print resetmode
                # This should remove all references to the pucks and effectively kill them off. If there were other
                # variables referring to this list, this would not stop the pucks.
                
                # Delete all the objects on the table. Cleaning out these list reference to these objects effectively
                # deletes the objects. Notice the controlled list must be cleared also.
                air_table.pucks = []
                air_table.controlled_pucks = []
                air_table.springs = []
                
                # Now just black out the screen.
                game_window.clear()
                
                # Reinitialize the demo.
                make_some_pucks( demo_mode)               
                        
            if ok_to_render:
                # Get input from network clients.
                game_server.Pump()
                env.checkForQuietClients()
                
            for client_name in env.clients:
                # Calculate client related forces.
                env.clients[client_name].calc_string_forces_on_pucks()
                
            if ok_to_render:
                # Control the zoom
                env.control_zoom_and_view()
                
                for controlled_puck in air_table.controlled_pucks:
                    # Rotate based on keyboard of the controlling client.
                    controlled_puck.jet.client_rotation_control( controlled_puck.client_name)
                    controlled_puck.gun.client_rotation_control( controlled_puck.client_name)
                    
                    # Turn gun on/off
                    controlled_puck.gun.control_firing( controlled_puck.client_name)
                    
                    # Turn shield on/off
                    controlled_puck.gun.control_shield( controlled_puck.client_name)
                    
            
            # Calculate jet forces on pucks...
            for controlled_puck in air_table.controlled_pucks:
                controlled_puck.jet.turn_jet_forces_onoff( controlled_puck.client_name)
            
            # Calculate the forces the springs apply on the pucks...
            for eachspring in air_table.springs:
                eachspring.calc_spring_forces_on_pucks()
                
            # Apply forces to the pucks and calculate movements.
            for eachpuck in air_table.pucks:
                air_table.update_PuckSpeedAndPosition( eachpuck, dt_physics_s * env.timeDirection)
            
            # Check for puck-wall and puck-puck collisions and make penetration corrections.
            air_table.check_for_collisions( dt_physics_s * env.timeDirection)
            
            if ok_to_render:
                
                # Erase the blackboard.
                if not env.inhibit_screen_clears:
                    if (air_table.perfect_kiss):
                        gray_level = 40
                        game_window.surface.fill((gray_level,gray_level,gray_level))
                    else:
                        if not air_table.g_ON:
                            game_window.surface.fill((0,0,0))  # Black
                        else:
                            #gray_level = 50
                            #game_window.surface.fill((gray_level,gray_level,gray_level))
                            game_window.surface.fill((0,82,110))  # Blue
                
                # Display game timer text.
                if air_table.FPS_display:
                    display_number( env.FR_avg.result, fnt_FPS, mode='FPS')
                    if (air_table.perfect_kiss):
                        display_number( air_table.collision_count, fnt_FPS, mode='counter')
                if (demo_mode == 7):
                    display_number( env.game_time_s, fnt_gameTimer, mode='gameTimer')
                if (env.generalTimer):
                    display_number( env.general_timer_s, fnt_generalTimer, mode='generalTimer')
                
                # Clean out old bullets.
                puck_list_copy = air_table.pucks[:]
                for thisPuck in puck_list_copy:
                    if (thisPuck.bullet) and ((env.time_s - thisPuck.birth_time_s) > thisPuck.age_limit_s):
                        air_table.pucks.remove(thisPuck)
                del puck_list_copy       
                
                # Now draw pucks, springs, mouse tethers, and jets.
                
                # Draw boundaries of table.
                air_table.draw()
                
                for eachpuck in air_table.pucks: 
                    eachpuck.draw()
                    if (eachpuck.jet != None):
                        if env.clients[eachpuck.client_name].active or (eachpuck.client_name == 'test'):
                            eachpuck.jet.draw()
                            eachpuck.gun.draw()
                    
                for eachspring in air_table.springs: 
                    eachspring.draw()
                
                env.remove_healthless_clients()
                
                for client_name in env.clients:
                    if (env.clients[client_name].selected_puck != None):
                        env.clients[client_name].draw_cursor_string()
                    
                    # Draw cursors for network clients.
                    if ((client_name != 'local') and env.clients[client_name].active):
                        env.clients[client_name].draw_fancy_server_cursor()
                    
                pygame.display.flip()
                dt_render_s = 0
            
            # Limit the rendering framerate to be below that of the physics calculations.
            dt_render_s += dt_gameLoop_s
            
            # Keep track of time for deleting old bullets.
            env.time_s += dt_gameLoop_s
            
            # Jello madness game timer
            if (air_table.tangled):
                env.game_time_s += dt_gameLoop_s
                
            if (env.generalTimer):
                env.general_timer_s += dt_gameLoop_s
            

#============================================================
# Run the main program.    
#============================================================
        
main()