<commit_before>import AsyncStorage from '@react-native-async-storage/async-storage';
import { length, lineString } from '@turf/turf';
import { Button, Card, Divider, Modal, Text, useTheme } from '@ui-kitten/components';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import propTypes from 'prop-types';
import React, { useEffect } from 'react';
import { Alert, StyleSheet, View } from 'react-native';
import { useMutation, useQueryClient } from 'react-query';
import * as Sentry from 'sentry-expo';
import { useAPI } from '../services/api';
import backgroundLocationService from '../services/backgroundLocation';
import config from '../services/config';
import { getIcon } from '../services/icons';
import { useOfflineCache } from '../services/offline';
import { PADDING, RADIUS } from '../services/styles';
import { lineCoordinatesToString } from '../services/utilities';
import Spinner from './Spinner';

const STORAGE_KEY = 'wvcr-vehicle-tracking-state';

TaskManager.defineTask(backgroundLocationService.taskName, async ({ data, error }) => {
  if (error) {
    console.log(error);
    Sentry.captureException(error);

    return;
  }

  if (data) {
    const { locations } = data;

    if (locations) {
      backgroundLocationService.notify(locations);
    }
  }
});

const buttonStatuses = {
  idle: 'basic',
  tracking: 'success',
  paused: 'danger',
};

export const initialVehicleTrackingState = {
  status: 'idle',
  isModalVisible: false,
  routeCoordinates: [],
  start: null,
  pickups: [],

  // these are derived...
  isPaused: false,
  isTracking: false,
  buttonStatus: buttonStatuses.idle,
};

export const vehicleTrackingReducer = (draft, action) => {
  switch (action.type) {
    case 'SHOW':
      draft.isModalVisible = true;

      break;

    case 'HIDE':
      draft.isModalVisible = false;

      break;

    case 'START':
      draft.status = 'tracking';
      draft.start = new Date();

      break;

    case 'PAUSE':
      draft.status = 'paused';
      draft.isModalVisible = false;

      break;

    case 'RESUME':
      draft.status = 'tracking';

      break;

    case 'ADD_PICKUP': {
      draft.pickups.push(action.payload);

      break;
    }

    case 'ADD_ROUTE_COORDINATES':
      draft.routeCoordinates = draft.routeCoordinates.concat(action.payload);

      break;
    case 'RESET':
      AsyncStorage.removeItem(STORAGE_KEY);

      return initialVehicleTrackingState;

    case 'RESTORE_FROM_STORAGE':
      return action.payload;

    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }

  // derived...
  draft.isPaused = draft.status === 'paused';
  draft.isTracking = draft.status === 'tracking' || draft.isPaused;
  draft.buttonStatus = buttonStatuses[draft.status];

  // backup in case phone crashes
  AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(draft));
};

const getDistance = (coords) => {
  if (coords.length < 2) {
    return 'n/a';
  }

  const arrayCoords = coords.map(({ latitude, longitude }) => [longitude, latitude]);

  const line = lineString(arrayCoords);

  const miles = length(line, { units: 'miles' });

  return `${miles.toFixed(2)} miles`;
};

export default function VehicleTracking({ state, dispatch, startTracking, resumeRoute }) {
  const theme = useTheme();

  useEffect(() => {
    return () => {
      backgroundLocationService.unsubscribe();
    };
  }, []);

  useEffect(() => {
    const checkStorage = async () => {
      const backup = await AsyncStorage.getItem(STORAGE_KEY);

      if (backup && JSON.stringify(state) !== backup) {
        const parsedBackup = JSON.parse(backup);
        parsedBackup.start = new Date(parsedBackup.start);

        dispatch({ type: 'RESTORE_FROM_STORAGE', payload: parsedBackup });

        if (parsedBackup.isTracking && !parsedBackup.isPaused) {
          startTracking();
        }
      }
    };

    checkStorage();
  }, []);

  const stopTracking = async () => {
    console.log('stopTracking');

    backgroundLocationService.unsubscribe();

    try {
      const hasStarted = await Location.hasStartedLocationUpdatesAsync(backgroundLocationService.taskName);

      if (hasStarted) {
        await Location.stopLocationUpdatesAsync(backgroundLocationService.taskName);
      }
    } catch (error) {
      console.log(error);
      Sentry.captureException(error);
    }
  };

  const pauseRoute = async () => {
    console.log('pauseRoute');

    await stopTracking();

    dispatch({ type: 'PAUSE' });
  };

  const cancelRoute = () => {
    console.log('cancelRoute');

    Alert.alert("Canceled routes are deleted and can't be submitted.", 'Please confirm choice:', [
      {
        text: 'Resume Route',
      },
      {
        text: 'Cancel Route',
        onPress: async () => {
          await stopTracking();

          dispatch({ type: 'RESET' });
        },
      },
    ]);
  };

  const { postReport, postRoute } = useAPI();
  const completeRoute = async () => {
    console.log('completeRoute');

    await stopTracking();

    dispatch({ type: 'HIDE' });

    mutation.mutate();
  };

  const confirmCompleteRoute = () => {
    Alert.alert(
      'Submit Route',
      `Are you sure that you want to stop tracking and submit this route with ${state.pickups.length} pickups?`,
      [
        {
          text: 'Yes',
          onPress: completeRoute,
        },
        {
          text: 'Cancel',
        },
      ]
    );
  };
  const [spinnerMessage, setSpinnerMessage] = React.useState('');

  const { isConnected, cacheRoute, cacheReport } = useOfflineCache();
  const submitRoute = async () => {
    const submitValues = {
      start_time: state.start,
      end_time: new Date(),
      geog: lineCoordinatesToString(state.routeCoordinates),
      submit_date: new Date().toISOString(),
    };

    if (!isConnected) {
      await cacheRoute(submitValues, state.pickups);

      dispatch({ type: 'RESET' });

      return;
    }

    setSpinnerMessage('submitting route...');

    let responseJson;
    try {
      responseJson = await postRoute(submitValues);
    } catch (error) {
      await cacheRoute(submitValues, state.pickups, error);

      dispatch({ type: 'RESET' });

      return;
    }

    const submitErrors = [];
    for (let index = 0; index < state.pickups.length; index++) {
      // create a new object since we are adding a prop
      const pickup = { ...state.pickups[index], route_id: responseJson.route_id };

      setSpinnerMessage(`submitting pickup ${index + 1} of ${state.pickups.length}...`);

      try {
        await postReport(pickup, config.REPORT_TYPES.pickup);
      } catch (error) {
        await cacheReport(pickup, error);
        submitErrors.push(error);
      }
    }

    if (submitErrors.length) {
      Alert.alert(
        'Error',
        `Your route has been submitted successfully, but there were errors submitting your pickups. They have been cached on your device for later submission. \n\n${submitErrors
          .map((error) => error.message)
          .join('\n')}`
      );
    } else {
      Alert.alert('Success!', 'Your route has been submitted successfully.');
    }
  };

  const queryClient = useQueryClient();
  const mutation = useMutation(submitRoute, {
    onSuccess: () => {
      queryClient.invalidateQueries(config.QUERY_KEYS.submissions);
      queryClient.invalidateQueries(config.QUERY_KEYS.profile);

      dispatch({ type: 'RESET' });
    },
    onError: (error) => {
      Alert.alert('Error', error.message);
    },
  });

  const CloseIcon = getIcon({
    pack: 'material-community',
    name: 'close',
    size: 25,
    color: theme['color-basic-700'],
  });
  const getHeader = (title) => (
    <View>
      <View style={styles.header}>
        <Text category="h5">{title}</Text>
        <Button accessoryLeft={CloseIcon} size="tiny" appearance="ghost" onPress={close} style={styles.closeButton} />
      </View>
    </View>
  );

  const close = () => {
    console.log('close pressed');
    dispatch({ type: 'HIDE' });
  };

  return (
    <>
      <Modal
        visible={state.isModalVisible}
        backdropStyle={{ backgroundColor: theme['color-basic-transparent-600'] }}
        onBackdropPress={close}
        style={styles.modal}
      >
        <Card disabled={true} header={getHeader('Route')} style={styles.modal}>
          <Text>Start: {state.start?.toLocaleString()}</Text>
          <Text>Status: {state.status}</Text>
          <Text>Distance: {getDistance(state.routeCoordinates)}</Text>
          <Text>Pickups: {state.pickups?.length}</Text>
          <Divider style={styles.divider} />
          {state.isPaused ? (
            <Button style={styles.button} onPress={resumeRoute}>
              Resume
            </Button>
          ) : (
            <Button style={styles.button} onPress={pauseRoute} status="danger">
              Pause Route
            </Button>
          )}
          <Button style={styles.button} onPress={confirmCompleteRoute} status="info">
            Finish and Submit Route
          </Button>
          <Button style={styles.button} onPress={cancelRoute} appearance="ghost">
            Cancel Route
          </Button>
        </Card>
      </Modal>
      <Spinner show={mutation.isLoading} message={spinnerMessage} />
    </>
  );
}

VehicleTracking.propTypes = {
  state: propTypes.object.isRequired,
  dispatch: propTypes.func.isRequired,
  startTracking: propTypes.func.isRequired,
  resumeRoute: propTypes.func.isRequired,
};

const styles = StyleSheet.create({
  modal: {
    borderRadius: RADIUS,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  divider: {
    marginVertical: PADDING,
  },
  button: {
    marginTop: PADDING,
  },
  closeButton: {
    marginRight: -10,
  },
});
<commit_msg>fix(front): prevent empty tracking route submissions<commit_after>import AsyncStorage from '@react-native-async-storage/async-storage';
import { length, lineString } from '@turf/turf';
import { Button, Card, Divider, Modal, Text, useTheme } from '@ui-kitten/components';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import propTypes from 'prop-types';
import React, { useEffect } from 'react';
import { Alert, StyleSheet, View } from 'react-native';
import { useMutation, useQueryClient } from 'react-query';
import * as Sentry from 'sentry-expo';
import { useAPI } from '../services/api';
import backgroundLocationService from '../services/backgroundLocation';
import config from '../services/config';
import { getIcon } from '../services/icons';
import { useOfflineCache } from '../services/offline';
import { PADDING, RADIUS } from '../services/styles';
import { lineCoordinatesToString } from '../services/utilities';
import Spinner from './Spinner';

const STORAGE_KEY = 'wvcr-vehicle-tracking-state';

TaskManager.defineTask(backgroundLocationService.taskName, async ({ data, error }) => {
  if (error) {
    console.log(error);
    Sentry.captureException(error);

    return;
  }

  if (data) {
    const { locations } = data;

    if (locations) {
      backgroundLocationService.notify(locations);
    }
  }
});

const buttonStatuses = {
  idle: 'basic',
  tracking: 'success',
  paused: 'danger',
};

export const initialVehicleTrackingState = {
  status: 'idle',
  isModalVisible: false,
  routeCoordinates: [],
  start: null,
  pickups: [],

  // these are derived...
  isPaused: false,
  isTracking: false,
  buttonStatus: buttonStatuses.idle,
};

export const vehicleTrackingReducer = (draft, action) => {
  switch (action.type) {
    case 'SHOW':
      draft.isModalVisible = true;

      break;

    case 'HIDE':
      draft.isModalVisible = false;

      break;

    case 'START':
      draft.status = 'tracking';
      draft.start = new Date();

      break;

    case 'PAUSE':
      draft.status = 'paused';
      draft.isModalVisible = false;

      break;

    case 'RESUME':
      draft.status = 'tracking';

      break;

    case 'ADD_PICKUP': {
      draft.pickups.push(action.payload);

      break;
    }

    case 'ADD_ROUTE_COORDINATES':
      draft.routeCoordinates = draft.routeCoordinates.concat(action.payload);

      break;
    case 'RESET':
      AsyncStorage.removeItem(STORAGE_KEY);

      return initialVehicleTrackingState;

    case 'RESTORE_FROM_STORAGE':
      return action.payload;

    default:
      throw new Error(`Unhandled action type: ${action.type}`);
  }

  // derived...
  draft.isPaused = draft.status === 'paused';
  draft.isTracking = draft.status === 'tracking' || draft.isPaused;
  draft.buttonStatus = buttonStatuses[draft.status];

  // backup in case phone crashes
  AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(draft));
};

const getDistance = (coords) => {
  if (coords.length < 2) {
    return 'n/a';
  }

  const arrayCoords = coords.map(({ latitude, longitude }) => [longitude, latitude]);

  const line = lineString(arrayCoords);

  const miles = length(line, { units: 'miles' });

  return `${miles.toFixed(2)} miles`;
};

export default function VehicleTracking({ state, dispatch, startTracking, resumeRoute }) {
  const theme = useTheme();

  useEffect(() => {
    return () => {
      backgroundLocationService.unsubscribe();
    };
  }, []);

  useEffect(() => {
    const checkStorage = async () => {
      const backup = await AsyncStorage.getItem(STORAGE_KEY);

      if (backup && JSON.stringify(state) !== backup) {
        const parsedBackup = JSON.parse(backup);
        parsedBackup.start = new Date(parsedBackup.start);

        dispatch({ type: 'RESTORE_FROM_STORAGE', payload: parsedBackup });

        if (parsedBackup.isTracking && !parsedBackup.isPaused) {
          startTracking();
        }
      }
    };

    checkStorage();
  }, []);

  const stopTracking = async () => {
    console.log('stopTracking');

    backgroundLocationService.unsubscribe();

    try {
      const hasStarted = await Location.hasStartedLocationUpdatesAsync(backgroundLocationService.taskName);

      if (hasStarted) {
        await Location.stopLocationUpdatesAsync(backgroundLocationService.taskName);
      }
    } catch (error) {
      console.log(error);
      Sentry.captureException(error);
    }
  };

  const pauseRoute = async () => {
    console.log('pauseRoute');

    await stopTracking();

    dispatch({ type: 'PAUSE' });
  };

  const cancelRoute = () => {
    console.log('cancelRoute');

    Alert.alert("Canceled routes are deleted and can't be submitted.", 'Please confirm choice:', [
      {
        text: 'Resume Route',
      },
      {
        text: 'Cancel Route',
        onPress: async () => {
          await stopTracking();

          dispatch({ type: 'RESET' });
        },
      },
    ]);
  };

  const { postReport, postRoute } = useAPI();
  const completeRoute = async () => {
    console.log('completeRoute');

    await stopTracking();

    dispatch({ type: 'HIDE' });

    mutation.mutate();
  };

  const confirmCompleteRoute = () => {
    if (state.routeCoordinates.length < 2) {
      Alert.alert('You must have tracked at least two positions to submit a route!');

      return;
    }

    Alert.alert(
      'Submit Route',
      `Are you sure that you want to stop tracking and submit this route with ${state.pickups.length} pickups?`,
      [
        {
          text: 'Yes',
          onPress: completeRoute,
        },
        {
          text: 'Cancel',
        },
      ]
    );
  };
  const [spinnerMessage, setSpinnerMessage] = React.useState('');

  const { isConnected, cacheRoute, cacheReport } = useOfflineCache();
  const submitRoute = async () => {
    const submitValues = {
      start_time: state.start,
      end_time: new Date(),
      geog: lineCoordinatesToString(state.routeCoordinates),
      submit_date: new Date().toISOString(),
    };

    if (!isConnected) {
      await cacheRoute(submitValues, state.pickups);

      dispatch({ type: 'RESET' });

      return;
    }

    setSpinnerMessage('submitting route...');

    let responseJson;
    try {
      responseJson = await postRoute(submitValues);
    } catch (error) {
      await cacheRoute(submitValues, state.pickups, error);

      dispatch({ type: 'RESET' });

      return;
    }

    const submitErrors = [];
    for (let index = 0; index < state.pickups.length; index++) {
      // create a new object since we are adding a prop
      const pickup = { ...state.pickups[index], route_id: responseJson.route_id };

      setSpinnerMessage(`submitting pickup ${index + 1} of ${state.pickups.length}...`);

      try {
        await postReport(pickup, config.REPORT_TYPES.pickup);
      } catch (error) {
        await cacheReport(pickup, error);
        submitErrors.push(error);
      }
    }

    if (submitErrors.length) {
      Alert.alert(
        'Error',
        `Your route has been submitted successfully, but there were errors submitting your pickups. They have been cached on your device for later submission. \n\n${submitErrors
          .map((error) => error.message)
          .join('\n')}`
      );
    } else {
      Alert.alert('Success!', 'Your route has been submitted successfully.');
    }
  };

  const queryClient = useQueryClient();
  const mutation = useMutation(submitRoute, {
    onSuccess: () => {
      queryClient.invalidateQueries(config.QUERY_KEYS.submissions);
      queryClient.invalidateQueries(config.QUERY_KEYS.profile);

      dispatch({ type: 'RESET' });
    },
    onError: (error) => {
      Alert.alert('Error', error.message);
    },
  });

  const CloseIcon = getIcon({
    pack: 'material-community',
    name: 'close',
    size: 25,
    color: theme['color-basic-700'],
  });
  const getHeader = (title) => (
    <View>
      <View style={styles.header}>
        <Text category="h5">{title}</Text>
        <Button accessoryLeft={CloseIcon} size="tiny" appearance="ghost" onPress={close} style={styles.closeButton} />
      </View>
    </View>
  );

  const close = () => {
    console.log('close pressed');
    dispatch({ type: 'HIDE' });
  };

  return (
    <>
      <Modal
        visible={state.isModalVisible}
        backdropStyle={{ backgroundColor: theme['color-basic-transparent-600'] }}
        onBackdropPress={close}
        style={styles.modal}
      >
        <Card disabled={true} header={getHeader('Route')} style={styles.modal}>
          <Text>Start: {state.start?.toLocaleString()}</Text>
          <Text>Status: {state.status}</Text>
          <Text>Distance: {getDistance(state.routeCoordinates)}</Text>
          <Text>Pickups: {state.pickups?.length}</Text>
          <Divider style={styles.divider} />
          {state.isPaused ? (
            <Button style={styles.button} onPress={resumeRoute}>
              Resume
            </Button>
          ) : (
            <Button style={styles.button} onPress={pauseRoute} status="danger">
              Pause Route
            </Button>
          )}
          <Button style={styles.button} onPress={confirmCompleteRoute} status="info">
            Finish and Submit Route
          </Button>
          <Button style={styles.button} onPress={cancelRoute} appearance="ghost">
            Cancel Route
          </Button>
        </Card>
      </Modal>
      <Spinner show={mutation.isLoading} message={spinnerMessage} />
    </>
  );
}

VehicleTracking.propTypes = {
  state: propTypes.object.isRequired,
  dispatch: propTypes.func.isRequired,
  startTracking: propTypes.func.isRequired,
  resumeRoute: propTypes.func.isRequired,
};

const styles = StyleSheet.create({
  modal: {
    borderRadius: RADIUS,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  divider: {
    marginVertical: PADDING,
  },
  button: {
    marginTop: PADDING,
  },
  closeButton: {
    marginRight: -10,
  },
});
<|endoftext|>


<commit_before>import PanelsActionCreator from '../actions/panels-action-creator';
import DisksActionCreator from '../actions/disks-action-creator';
import SculptureActionCreator from '../actions/sculpture-action-creator';
import Disk from '../utils/disk';
import TrackedData from '../utils/tracked-data';
import DiskModel from '../utils/DiskModel';

const positivePanels = ['5','6','7','8','9'];
const negativePanels = ['4','3','2','1','0'];

export default class DiskGameLogic {
  // These are automatically added to the sculpture store
  static trackedProperties = {
    level: 0,
    disks: new TrackedData({
      disk0: new Disk(),
      disk1: new Disk(),
      disk2: new Disk()
    }),
  };

  constructor(store, config) {
    this.store = store;
    this.config = config;
    this.gameConfig = config.DISK_GAME;

    this.physicalDisks = {
      disk0: new DiskModel(),
      disk1: new DiskModel(),
      disk2: new DiskModel(),
    };

    this.sculptureActionCreator = new SculptureActionCreator(this.store.dispatcher);
    this.diskActions = new DisksActionCreator(this.store.dispatcher);

    this._complete = false;

    this.startDisks();
  }

  get data() {
    return this.store.data.get('disk');
  }

  get _lights() {
    return this.store.data.get('lights');
  }

  start() {
    this._level = 0;
    this._complete = false;

    // Activate shadow lights
    for (let stripId of Object.keys(this.gameConfig.SHADOW_LIGHTS)) {
      const panels = this.gameConfig.SHADOW_LIGHTS[stripId];
      for (let panelId of Object.keys(panels)) {
        this._lights.setIntensity(stripId, panelId, this.gameConfig.SHADOW_LIGHT_INTENSITY);
      }
    }

    // Activate UI indicators
    const lightArray = this._lights;
    for (const stripId of Object.keys(this.gameConfig.CONTROL_MAPPINGS.STRIP_TO_DISK)) {
      lightArray.setColor(stripId, null, this.gameConfig.CONTROL_PANEL_COLOR);
      for (let i=0;i<5;i++) {
        lightArray.setIntensity(stripId, positivePanels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
        lightArray.setIntensity(stripId, negativePanels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
      }
    }

    this.startLevel();
  }

  startLevel() {
    // Set initial position
    const disks = this.data.get('disks');
    for (let diskId of Object.keys(this._levelConfig.disks)) {
      const disk = disks.get(diskId);
      const pos = this._levelConfig.disks[diskId];
      disk.setPosition(pos);
      this.physicalDisks[diskId].targetPosition = pos;
    }
  }

  // Start physical disk model
  startDisks() {
    Object.keys(this.physicalDisks).forEach((diskId) => {
      this.physicalDisks[diskId].start();
      this.physicalDisks[diskId].on('position', (pos) => {
        this.sendDiskUpdate(diskId, pos);
      });
    });
    this.interval = setInterval(() => {
      Object.keys(this.physicalDisks).forEach((key) => this.physicalDisks[key].tick());
    }, 30);
  }

  endDisks() {
    clearInterval(this.interval);
    delete this.interval;
    Object.keys(this.physicalDisks).forEach((key) => this.physicalDisks[key].removeAllListeners('position'));
  }

  // FIXME: These end() methods may be obsolete now since everything is reset before every game anyway
  end() {
    this.config.GAME_STRIPS.forEach((id) => this._lights.setIntensity(id, null, 0));
    // Deactivate shadow lights
    for (let stripId of Object.keys(this.gameConfig.SHADOW_LIGHTS)) {
      const panels = this.gameConfig.SHADOW_LIGHTS[stripId];
      for (let panelId of Object.keys(panels)) {
        this._lights.setIntensity(stripId, panelId, 0);
      }
    }
  }

  handleActionPayload(payload) {
    const actionHandlers = {
      [PanelsActionCreator.PANEL_PRESSED]: this._actionPanelPressed.bind(this),
      [DisksActionCreator.DISK_UPDATE]: this._actionDiskUpdate.bind(this),
      [SculptureActionCreator.FINISH_STATUS_ANIMATION]: this._actionFinishStatusAnimation.bind(this),
      [SculptureActionCreator.MERGE_STATE]: this._actionMergeState.bind(this),
    };

    const actionHandler = actionHandlers[payload.actionType];
    if (actionHandler) {
      actionHandler(payload);
    }
  }

  /* FIXME: Handle start of game

    // Handle game changes
    if (changes.hasOwnProperty('currentGame')) {
      // Reset on start of playing the disk game and on start of games cycle
      if (changes.currentGame === GAMES.DISK || changes.currentGame === GAMES.HANDSHAKE) {
        setTimeout(this.resetDisks.bind(this), 0);
      }
    }

   */

  resetDisks() {
    Object.keys(this.physicalDisks).forEach((diskId) => {
      this.physicalDisks[diskId].stop();
      const pos = this.disks.get(diskId).get('position');
      this.physicalDisks[diskId].targetPosition = pos;
      this.sendDiskUpdate(diskId, pos);
    });
  }

  sendDiskUpdate(diskId, position) {
    this.diskActions.sendDiskUpdate(diskId, { position });
  }

  /**
   * Called on any change in panel state
   */
  _actionPanelPressed(payload) {
    // FIXME: Break up this method
    if (this._complete) return;

    const controlMappings = this.gameConfig.CONTROL_MAPPINGS;
    const {stripId, panelId} = payload;

    const diskId = controlMappings.STRIP_TO_DISK[stripId];
    // The event doesn't concern us - use default behavior
    if (diskId === undefined) return;

    const disk = this.data.get('disks').get(diskId);

    // For each strip change:
    //   Find highest positive activated panel
    //   Find highest negative activated panel
    //   if both positive and negative:
    //     Set conflict
    //   else
    //     set target speed, or 0 if no panels are active

    const lightArray = this._lights;
    const panels = lightArray.get(stripId).get('panels');
    const isActive = (panelId) => panels.get(panelId).get('active');
    let positivePanel = positivePanels.findIndex(isActive);
    let negativePanel = negativePanels.findIndex(isActive);

    if (positivePanel >= 0 && negativePanel >= 0) {
      // FIXME: Set conflict
      this._setDiskColor(stripId, this.gameConfig.CONFLICT_INTENSITY, this.config.COLORS.ERROR);
    }
    else {
      let speed = 0;
      let sign = 1;
      let panelIds = [];
      if (positivePanel === -1 && negativePanel === -1) {
        speed = 0;
      }
      else if (positivePanel >= 0) {
        speed = (positivePanel + 1);
        for (let i=0;i<=positivePanel;i++) panelIds.push(positivePanels[i]);
      }
      else {
        speed = (negativePanel + 1);
        sign = -1;
        for (let i=0;i<=negativePanel;i++) panelIds.push(negativePanels[i]);
      }

      const newspeed = speed === 0 ? 0 : sign * this.gameConfig.SPEEDS[speed - 1];
      disk.setTargetSpeed(newspeed);
      this.physicalDisks[diskId].targetSpeed = newspeed;

      // On speed changes, publish position
      disk.setPosition(this.store.getDiskPosition(diskId));
      // ..and check win condition if master
      if (this.store.isMaster && !this.store.isStatusSuccess) {
        this._checkWinConditions();
      }

      const lightArray = this._lights;
      const setPanels = (panels, index) => {
        for (let i=0;i<5;i++) {
          if (index >= i) {
            lightArray.setIntensity(stripId, panels[i], this.gameConfig.ACTIVE_CONTROL_PANEL_INTENSITY);
            lightArray.setColor(stripId, panels[i], this.store.locationColor);
          }
          else {
            lightArray.setIntensity(stripId, panels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
            lightArray.setColor(stripId, panels[i], this.gameConfig.CONTROL_PANEL_COLOR);
          }
        }
      };
      setPanels(positivePanels, sign > 0 ? positivePanel : -1);
      setPanels(negativePanels, sign < 0 ? negativePanel : -1);
    }
  }

  /**
   * Called when disks move (comes from the physical disk model)
   */
  _actionDiskUpdate(payload) {
    if (this._complete) return;
    // FIXME: Only needed for emulator?
    const {diskId, position} = payload;
    this.physicalDisks[diskId].targetPosition = position;
    if (this.store.isMaster && !this.store.isStatusSuccess) {
      this._checkWinConditions();
    }
  }

  /*!
   * Merge remote state
   */
  _actionMergeState(payload) {
    if (payload.hasOwnProperty('disk')) this._mergeDisk(payload.disk, payload.metadata.props.disk);
  }

  _mergeDisk(diskChanges, props = {}) {
    // Master owns the level field
    if (!this.store.isMaster) {
      if (diskChanges.hasOwnProperty('level')) {
        this.data.set('level', diskChanges.level, props.level);
      }
    }

    if (!diskChanges.disks) return;

    const disksChanges = diskChanges.disks;
    const disksProps = props.disks;

    const currDisks = this.data.get('disks');

    for (let diskId of Object.keys(disksChanges)) {
      const changedDisk = disksChanges[diskId];
      const diskProps = disksProps[diskId];
      const currDisk = currDisks.get(diskId);
      if (changedDisk.hasOwnProperty('position')) {
        currDisk.setPosition(changedDisk.position, diskProps.position);
        this.physicalDisks[diskId].targetPosition = changedDisk.position;
        if (this.store.isMaster && !this.store.isStatusSuccess) {
          this._checkWinConditions();
        }
      }
      if (changedDisk.hasOwnProperty('user')) {
        currDisk.setUser(changedDisk.user, diskProps.user);
      }
      if (changedDisk.hasOwnProperty('targetSpeed')) {
        currDisk.setTargetSpeed(changedDisk.targetSpeed, diskProps.targetSpeed);
        this.physicalDisks[diskId].targetSpeed = changedDisk.targetSpeed;
      }
    }
  }

  _actionFinishStatusAnimation() {
    if (this._complete) {
      setTimeout(() => this.sculptureActionCreator.sendStartNextGame(), 4000);
    }
  }

  _setDiskColor(stripId, intensity, color) {
    const lightArray = this._lights;
    lightArray.setIntensity(stripId, null, intensity);
    lightArray.setColor(stripId, null, color);
  }

  /**
   * Calculates how far away from the correct zone the current disk position is.
   */
  _distanceFromZone(pos, zone) {

    const angle = (zone[1] - zone[0] + 360) % 360;
    const d = angle / 2;
    const c = (zone[0] + d) % 360;

    // Smallest difference between two angles
    const e = 180 - Math.abs(Math.abs(pos - c) - 180);

    const err = Math.max(e-d, 0);
    return err;
  }

  /**
   * Win conditions:
   * - Each marker must have it's rules match against the position range of all 3 disks
   */
  _checkWinConditions() {
    const disks = this.data.get('disks');
    const score = this.getScore(disks);
    const isMoving = Array.from(disks).some((diskId) => Math.abs(this.getDiskSpeed(diskId)) > 0);

//    console.debug(`Total error: ${totalError}`);
    if (!isMoving && score <= this.gameConfig.ABSOLUTE_TOLERANCE) {
      this._winGame();
    }
  }

  // FIXME: move these public methods up
  getDiskError(diskId) {
    // We cannot calculate the score of a complete game as we don't have a valid level
    if (this._complete) return 0;

    if (this._levelConfig.rule === 'absolute') {
      let pos = this.getLocalDiskPosition(diskId);
      if (pos > 180) pos -= 360;
      return Math.abs(pos);
    }
    else { // this._levelConfig.rule === 'relative'
      // Central and outer disk: get angle to middle disk
      // Middle disk: get smallest of angles to central and outer disks

      let pos = 0;
      if (diskId == 'disk1') { // Middle disk
        pos = Math.min.apply(Math, [
          Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk0')),
          Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk2'))
        ].map((pos) => pos > 180 ? 360 - pos : pos));
      }
      else { // Central or Outer disk
        pos = Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk1'));
        if (pos > 180) pos = 360 - pos;
      }
      return pos;
    }
  }

  getDiskSpeed(diskId) {
    const disks = this.data.get('disks');
    return disks.get(diskId).getTargetSpeed();
  }

  /**
   * Current score (the total number of degrees away from solution).
   * For 3 disks, this will be between 0 and 540
   */
  getScore(disks) {
    // We cannot calculate the score of a complete game as we don't have a valid level
    if (this._complete) return 0;

    let score = 0;
    if (this._levelConfig.rule === 'absolute') {
      for (let diskId of disks) {
        const err = this.getDiskError(diskId);
        score += err;
      }
    }
    else { // this._levelConfig.rule === 'relative'
      score = Math.max.apply(Math, [Math.abs(this.getLocalDiskPosition('disk0') - this.getLocalDiskPosition('disk1')),
                            Math.abs(this.getLocalDiskPosition('disk0') - this.getLocalDiskPosition('disk2'))]);
    }

    return score;
  }

  _winGame() {
    if (this.store.isStatusSuccess) return;

    this.store.data.get('lights').deactivateAll();
    this._stopAllDisks();

    this.store.setSuccessStatus();

    let level = this._level + 1;
    if (level >= this._levels) {
      this._complete = true;
    }
    else {
      this.startLevel();
    }

    this._level = level;
  }

  _stopAllDisks() {
    const disks = this.data.get('disks');

    for (let diskId of disks) {
      disks.get(diskId).stop();
    }
  }

  get _levelConfig() {
    return this.gameConfig.LEVELS[this._level];
  }

  get _levels() {
    return this.gameConfig.LEVELS.length;
  }

  get _level() {
    return this.data.get('level');
  }

  set _level(value) {
    return this.data.set('level', value);
  }

  getLocalDiskPosition(diskId) {
    return this.physicalDisks[diskId].position;
  }

}
<commit_msg>Fixed eslint errors<commit_after>import PanelsActionCreator from '../actions/panels-action-creator';
import DisksActionCreator from '../actions/disks-action-creator';
import SculptureActionCreator from '../actions/sculpture-action-creator';
import Disk from '../utils/disk';
import TrackedData from '../utils/tracked-data';
import DiskModel from '../utils/DiskModel';

const positivePanels = ['5','6','7','8','9'];
const negativePanels = ['4','3','2','1','0'];

export default class DiskGameLogic {
  // These are automatically added to the sculpture store
  static trackedProperties = {
    level: 0,
    disks: new TrackedData({
      disk0: new Disk(),
      disk1: new Disk(),
      disk2: new Disk()
    }),
  };

  constructor(store, config) {
    this.store = store;
    this.config = config;
    this.gameConfig = config.DISK_GAME;

    this.physicalDisks = {
      disk0: new DiskModel(),
      disk1: new DiskModel(),
      disk2: new DiskModel(),
    };

    this.sculptureActionCreator = new SculptureActionCreator(this.store.dispatcher);
    this.diskActions = new DisksActionCreator(this.store.dispatcher);

    this._complete = false;

    this.startDisks();
  }

  get data() {
    return this.store.data.get('disk');
  }

  get _lights() {
    return this.store.data.get('lights');
  }

  start() {
    this._level = 0;
    this._complete = false;

    // Activate shadow lights
    for (let stripId of Object.keys(this.gameConfig.SHADOW_LIGHTS)) {
      const panels = this.gameConfig.SHADOW_LIGHTS[stripId];
      for (let panelId of Object.keys(panels)) {
        this._lights.setIntensity(stripId, panelId, this.gameConfig.SHADOW_LIGHT_INTENSITY);
      }
    }

    // Activate UI indicators
    const lightArray = this._lights;
    for (const stripId of Object.keys(this.gameConfig.CONTROL_MAPPINGS.STRIP_TO_DISK)) {
      lightArray.setColor(stripId, null, this.gameConfig.CONTROL_PANEL_COLOR);
      for (let i=0;i<5;i++) {
        lightArray.setIntensity(stripId, positivePanels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
        lightArray.setIntensity(stripId, negativePanels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
      }
    }

    this.startLevel();
  }

  startLevel() {
    // Set initial position
    const disks = this.data.get('disks');
    for (let diskId of Object.keys(this._levelConfig.disks)) {
      const disk = disks.get(diskId);
      const pos = this._levelConfig.disks[diskId];
      disk.setPosition(pos);
      this.physicalDisks[diskId].targetPosition = pos;
    }
  }

  // Start physical disk model
  startDisks() {
    Object.keys(this.physicalDisks).forEach((diskId) => {
      this.physicalDisks[diskId].start();
      this.physicalDisks[diskId].on('position', (pos) => {
        this.sendDiskUpdate(diskId, pos);
      });
    });
    this.interval = setInterval(() => {
      Object.keys(this.physicalDisks).forEach((key) => this.physicalDisks[key].tick());
    }, 30);
  }

  endDisks() {
    clearInterval(this.interval);
    delete this.interval;
    Object.keys(this.physicalDisks).forEach((key) => this.physicalDisks[key].removeAllListeners('position'));
  }

  // FIXME: These end() methods may be obsolete now since everything is reset before every game anyway
  end() {
    this.config.GAME_STRIPS.forEach((id) => this._lights.setIntensity(id, null, 0));
    // Deactivate shadow lights
    for (let stripId of Object.keys(this.gameConfig.SHADOW_LIGHTS)) {
      const panels = this.gameConfig.SHADOW_LIGHTS[stripId];
      for (let panelId of Object.keys(panels)) {
        this._lights.setIntensity(stripId, panelId, 0);
      }
    }
  }

  handleActionPayload(payload) {
    const actionHandlers = {
      [PanelsActionCreator.PANEL_PRESSED]: this._actionPanelPressed.bind(this),
      [DisksActionCreator.DISK_UPDATE]: this._actionDiskUpdate.bind(this),
      [SculptureActionCreator.FINISH_STATUS_ANIMATION]: this._actionFinishStatusAnimation.bind(this),
      [SculptureActionCreator.MERGE_STATE]: this._actionMergeState.bind(this),
    };

    const actionHandler = actionHandlers[payload.actionType];
    if (actionHandler) {
      actionHandler(payload);
    }
  }

  /* FIXME: Handle start of game

    // Handle game changes
    if (changes.hasOwnProperty('currentGame')) {
      // Reset on start of playing the disk game and on start of games cycle
      if (changes.currentGame === GAMES.DISK || changes.currentGame === GAMES.HANDSHAKE) {
        setTimeout(this.resetDisks.bind(this), 0);
      }
    }

   */

  resetDisks() {
    Object.keys(this.physicalDisks).forEach((diskId) => {
      this.physicalDisks[diskId].stop();
      const pos = this.disks.get(diskId).get('position');
      this.physicalDisks[diskId].targetPosition = pos;
      this.sendDiskUpdate(diskId, pos);
    });
  }

  sendDiskUpdate(diskId, position) {
    this.diskActions.sendDiskUpdate(diskId, { position });
  }

  /**
   * Called on any change in panel state
   */
  _actionPanelPressed(payload) {
    // FIXME: Break up this method
    if (this._complete) return;

    const controlMappings = this.gameConfig.CONTROL_MAPPINGS;
    const {stripId, panelId} = payload;

    const diskId = controlMappings.STRIP_TO_DISK[stripId];
    // The event doesn't concern us - use default behavior
    if (diskId === undefined) return;

    const disk = this.data.get('disks').get(diskId);

    // For each strip change:
    //   Find highest positive activated panel
    //   Find highest negative activated panel
    //   if both positive and negative:
    //     Set conflict
    //   else
    //     set target speed, or 0 if no panels are active

    const lightArray = this._lights;
    const panels = lightArray.get(stripId).get('panels');
    const isActive = (panelId) => panels.get(panelId).get('active');
    let positivePanel = positivePanels.findIndex(isActive);
    let negativePanel = negativePanels.findIndex(isActive);

    if (positivePanel >= 0 && negativePanel >= 0) {
      // FIXME: Set conflict
      this._setDiskColor(stripId, this.gameConfig.CONFLICT_INTENSITY, this.config.COLORS.ERROR);
    }
    else {
      let speed = 0;
      let sign = 1;
      let panelIds = [];
      if (positivePanel === -1 && negativePanel === -1) {
        speed = 0;
      }
      else if (positivePanel >= 0) {
        speed = (positivePanel + 1);
        for (let i=0;i<=positivePanel;i++) panelIds.push(positivePanels[i]);
      }
      else {
        speed = (negativePanel + 1);
        sign = -1;
        for (let i=0;i<=negativePanel;i++) panelIds.push(negativePanels[i]);
      }

      const newspeed = speed === 0 ? 0 : sign * this.gameConfig.SPEEDS[speed - 1];
      disk.setTargetSpeed(newspeed);
      this.physicalDisks[diskId].targetSpeed = newspeed;

      // On speed changes, publish position
      disk.setPosition(this.store.getDiskPosition(diskId));
      // ..and check win condition if master
      if (this.store.isMaster && !this.store.isStatusSuccess) {
        this._checkWinConditions();
      }

      const lightArray = this._lights;
      const setPanels = (panels, index) => {
        for (let i=0;i<5;i++) {
          if (index >= i) {
            lightArray.setIntensity(stripId, panels[i], this.gameConfig.ACTIVE_CONTROL_PANEL_INTENSITY);
            lightArray.setColor(stripId, panels[i], this.store.locationColor);
          }
          else {
            lightArray.setIntensity(stripId, panels[i], this.gameConfig.CONTROL_PANEL_INTENSITIES[i]);
            lightArray.setColor(stripId, panels[i], this.gameConfig.CONTROL_PANEL_COLOR);
          }
        }
      };
      setPanels(positivePanels, sign > 0 ? positivePanel : -1);
      setPanels(negativePanels, sign < 0 ? negativePanel : -1);
    }
  }

  /**
   * Called when disks move (comes from the physical disk model)
   */
  _actionDiskUpdate(payload) {
    if (this._complete) return;
    // FIXME: Only needed for emulator?
    const {diskId, position} = payload;
    this.physicalDisks[diskId].targetPosition = position;
    if (this.store.isMaster && !this.store.isStatusSuccess) {
      this._checkWinConditions();
    }
  }

  /*!
   * Merge remote state
   */
  _actionMergeState(payload) {
    if (payload.hasOwnProperty('disk')) this._mergeDisk(payload.disk, payload.metadata.props.disk);
  }

  _mergeDisk(diskChanges, props = {}) {
    // Master owns the level field
    if (!this.store.isMaster) {
      if (diskChanges.hasOwnProperty('level')) {
        this.data.set('level', diskChanges.level, props.level);
      }
    }

    if (!diskChanges.disks) return;

    const disksChanges = diskChanges.disks;
    const disksProps = props.disks;

    const currDisks = this.data.get('disks');

    for (let diskId of Object.keys(disksChanges)) {
      const changedDisk = disksChanges[diskId];
      const diskProps = disksProps[diskId];
      const currDisk = currDisks.get(diskId);
      if (changedDisk.hasOwnProperty('position')) {
        currDisk.setPosition(changedDisk.position, diskProps.position);
        this.physicalDisks[diskId].targetPosition = changedDisk.position;
        if (this.store.isMaster && !this.store.isStatusSuccess) {
          this._checkWinConditions();
        }
      }
      if (changedDisk.hasOwnProperty('user')) {
        currDisk.setUser(changedDisk.user, diskProps.user);
      }
      if (changedDisk.hasOwnProperty('targetSpeed')) {
        currDisk.setTargetSpeed(changedDisk.targetSpeed, diskProps.targetSpeed);
        this.physicalDisks[diskId].targetSpeed = changedDisk.targetSpeed;
      }
    }
  }

  _actionFinishStatusAnimation() {
    if (this._complete) {
      setTimeout(() => this.sculptureActionCreator.sendStartNextGame(), 4000);
    }
  }

  _setDiskColor(stripId, intensity, color) {
    const lightArray = this._lights;
    lightArray.setIntensity(stripId, null, intensity);
    lightArray.setColor(stripId, null, color);
  }

  /**
   * Calculates how far away from the correct zone the current disk position is.
   */
  _distanceFromZone(pos, zone) {

    const angle = (zone[1] - zone[0] + 360) % 360;
    const d = angle / 2;
    const c = (zone[0] + d) % 360;

    // Smallest difference between two angles
    const e = 180 - Math.abs(Math.abs(pos - c) - 180);

    const err = Math.max(e-d, 0);
    return err;
  }

  /**
   * Win conditions:
   * - Each marker must have it's rules match against the position range of all 3 disks
   */
  _checkWinConditions() {
    const disks = this.data.get('disks');
    const score = this.getScore(disks);
    const isMoving = Array.from(disks).some((diskId) => Math.abs(this.getDiskSpeed(diskId)) > 0);

//    console.debug(`Total error: ${totalError}`);
    if (!isMoving && score <= this.gameConfig.ABSOLUTE_TOLERANCE) {
      this._winGame();
    }
  }

  // FIXME: move these public methods up
  getDiskError(diskId) {
    // We cannot calculate the score of a complete game as we don't have a valid level
    if (this._complete) return 0;

    if (this._levelConfig.rule === 'absolute') {
      let pos = this.getLocalDiskPosition(diskId);
      if (pos > 180) pos -= 360;
      return Math.abs(pos);
    }
    else { // this._levelConfig.rule === 'relative'
      // Central and outer disk: get angle to middle disk
      // Middle disk: get smallest of angles to central and outer disks

      let pos = 0;
      if (diskId === 'disk1') { // Middle disk
        pos = Math.min.apply(Math, [
          Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk0')),
          Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk2'))
        ].map((pos) => pos > 180 ? 360 - pos : pos));
      }
      else { // Central or Outer disk
        pos = Math.abs(this.getLocalDiskPosition(diskId) - this.getLocalDiskPosition('disk1'));
        if (pos > 180) pos = 360 - pos;
      }
      return pos;
    }
  }

  getDiskSpeed(diskId) {
    const disks = this.data.get('disks');
    return disks.get(diskId).getTargetSpeed();
  }

  /**
   * Current score (the total number of degrees away from solution).
   * For 3 disks, this will be between 0 and 540
   */
  getScore(disks) {
    // We cannot calculate the score of a complete game as we don't have a valid level
    if (this._complete) return 0;

    let score = 0;
    if (this._levelConfig.rule === 'absolute') {
      for (let diskId of disks) {
        const err = this.getDiskError(diskId);
        score += err;
      }
    }
    else { // this._levelConfig.rule === 'relative'
      score = Math.max.apply(Math, [
        Math.abs(this.getLocalDiskPosition('disk0') - this.getLocalDiskPosition('disk1')),
        Math.abs(this.getLocalDiskPosition('disk0') - this.getLocalDiskPosition('disk2'))
      ]);
    }

    return score;
  }

  _winGame() {
    if (this.store.isStatusSuccess) return;

    this.store.data.get('lights').deactivateAll();
    this._stopAllDisks();

    this.store.setSuccessStatus();

    let level = this._level + 1;
    if (level >= this._levels) {
      this._complete = true;
    }
    else {
      this.startLevel();
    }

    this._level = level;
  }

  _stopAllDisks() {
    const disks = this.data.get('disks');

    for (let diskId of disks) {
      disks.get(diskId).stop();
    }
  }

  get _levelConfig() {
    return this.gameConfig.LEVELS[this._level];
  }

  get _levels() {
    return this.gameConfig.LEVELS.length;
  }

  get _level() {
    return this.data.get('level');
  }

  set _level(value) {
    return this.data.set('level', value);
  }

  getLocalDiskPosition(diskId) {
    return this.physicalDisks[diskId].position;
  }

}
<|endoftext|>


<commit_before>const
    Matrix = require('../index.js').Matrix,
    Complex = require('../index.js').Complex,
    CMatrix = require('../index.js').CMatrix,
    should = require('should');

describe('Matrix', function() {
  var mat;

  it('#Matrix() should be a function', function() {
    Matrix.should.be.a.Function;
  });

  it('should throw error when tried creating matrix without rows and columns arguments', function() {
    (function() { new Matrix(); }).should.throw('Tried creating matrix without rows and columns arguments');
    (function() { new Matrix(1); }).should.throw('Tried creating matrix without rows and columns arguments');
    (function() { new Matrix(1, 2); }).should.not.throw();
  });

  it('should be invoked with arguments rows and columns and return an object', function() {
    Matrix(1,2).should.be.an.Object;
    (new Matrix(3, 4)).should.be.an.Object;
  });

  it('#Matrix(3, 2) should return the matrix of size 3x2', function() {
    mat = Matrix(3, 2);
    mat.rows().should.equal(3);
    mat.cols().should.equal(2);
  })

  beforeEach(function() {
    mat = Matrix(3, 3)
      .set(0, 0, 1)
      .set(0, 1, 2)
      .set(0, 2, 3)
      .set(1, 0, 4)
      .set(1, 1, 5)
      .set(1, 2, 6)
      .set(2, 0, 7)
      .set(2, 1, 8)
      .set(2, 2, 9);
  });

  it('#set() should throw message when row or column nubers are out of range', function() {
    mat.set.should.be.a.Function;

    (function() {
      mat.set(3, 0, 68);
    }).should.throw('Row or column numbers are out of range');
    (function() {
      mat.set(-1, -2, 500);
    }).should.throw('Row or column numbers are out of range');
  });

  it('#set() with array argument should work ok', function() {
    mat.set.should.be.a.Function;

    Matrix(3, 3).set([
      1, 2, 3,
      4, 5, 6,
      7, 8, 9
    ]).toString().should.eql(mat.toString());

    (function() {
      Matrix(3, 3).set([
        1, 2, 3,
        4, 5, 6
      ]);
    }).should.throw('Too few coefficients passed to Matrix');

    (function() {
      Matrix(3, 3).set([
        1,  2,  3,
        4,  5,  6,
        7,  8,  9,
       10, 11, 12
      ]);
    }).should.throw('Too many coefficients passed to Matrix');
  });

  it('#get() should return the element value of Matrix', function() {
    mat.get.should.be.a.Function;

    mat.get(0, 0).should.equal(1);
    mat.get(0, 1).should.equal(2);
    mat.get(0, 2).should.equal(3);
    mat.get(1, 0).should.equal(4);
    mat.get(1, 1).should.equal(5);
    mat.get(1, 2).should.equal(6);
    mat.get(2, 0).should.equal(7);
    mat.get(2, 1).should.equal(8);
    mat.get(2, 2).should.equal(9);
    (function(){
      mat.get(3, 0);
    }).should.throw('Row or column numbers are out of range');
  });

  it('#toString() should return all element values of Matrix', function() {
    mat.toString.should.be.a.Function;

    mat.toString().should.equal("1 2 3\n4 5 6\n7 8 9");
  });

  it('#add() should return the sum of two matrices', function() {
    mat.add.should.be.a.Function;

    Matrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]).add(mat).toString().should.equal(" 3  6  9\n12 14 16\n18 20 22");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).add(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#add() should return a CMatrix with the sum of a matrix and a complex matrix', function() {
    mat.add.should.be.a.Function;

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    mat.add(cmat).toString().should.equal(" (3,0)  (6,0)  (9,0)\n(12,0) (14,0) (16,0)\n(18,0) (20,0) (22,0)");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).add(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#adda() should return the sum of two matrices and saves it back', function() {
    mat.adda.should.be.a.Function;

    mat.adda(
      Matrix(3, 3)
      .set([
        2,  4,  6,
        8,  9, 10,
       11, 12, 13
      ])
    );
    mat.toString().should.equal(" 3  6  9\n12 14 16\n18 20 22");

    (function() {
      mat.adda(
        Matrix(2, 3)
        .set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    (mat.adda(cmat) == undefined).should.be.true;
  });

  it('#sub() should return the difference of two matrices', function() {
    mat.sub.should.be.a.Function;

    Matrix(3, 3).set([
      1,  3,  5,
      7,  9, 11,
     13, 15, 17
    ]).sub(mat).toString().should.equal("0 1 2\n3 4 5\n6 7 8");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).sub(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#sub() should return a CMatrix with the sum of a matrix and complex matrix', function() {
    mat.add.should.be.a.Function;

    var cmat = CMatrix(3, 3).set([
      Complex( 2, 0), Complex( 4, 1),  Complex(6, 2),
      Complex( 8, 4), Complex( 9, 5), Complex(10, 6),
      Complex(11, 7), Complex(12, 8), Complex(13, 9)
    ]);

    mat.sub(cmat).toString().should.equal(" (-1,0) (-2,-1) (-3,-2)\n(-4,-4) (-4,-5) (-4,-6)\n(-4,-7) (-4,-8) (-4,-9)");

    (function() {
      mat.sub(
        CMatrix(2, 3).set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");
  });

  it('#suba() should return the difference of two matrices and saves it back', function() {
    mat.suba.should.be.a.Function;

    var mat2 = Matrix(3, 3)
    .set([
      1,  3,  5,
      7,  9, 11,
     13, 15, 17
    ]);
    mat2.suba(mat);
    mat2.toString().should.equal("0 1 2\n3 4 5\n6 7 8");

    (function() {
      mat.suba(
        Matrix(2, 3)
        .set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    (mat.suba(cmat) == undefined).should.be.true;
  });

  it('#mul() should return the product of two matrices', function() {
    mat.mul.should.be.a.Function;

    var vector = new Matrix(3, 1).set([1, 2, 3]);
    mat.mul(vector).toString().should.equal("14\n32\n50");

    (function() {
      vector.mul(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mul() should return a CMatrix with the product of a matrix and a complex matrix', function() {
    mat.mul.should.be.a.Function;

    var cvector = new CMatrix(3, 1).set([Complex(1, 1), Complex(2, 2), Complex(3, 3)]);
    mat.mul(cvector).toString().should.equal("14\n14\n32");

    (function() {
      cvector.mul(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mul() should return the product of a matrix and a scalar', function() {
    mat.mul.should.be.a.Function;

    mat.mul(-1).toString().should.equal("-1 -2 -3\n-4 -5 -6\n-7 -8 -9");
  });

  it('#mula() should return the product of two matrices and saves it back', function() {
    mat.mula.should.be.a.Function;

    var vector = new Matrix(3, 1).set([1, 2, 3]);
    mat.mula(vector);
    mat.toString().should.equal("14\n32\n50");

    (function() {
      vector.mula(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mula() should return the product of a matrices and a scalar value then saves it back', function() {
    mat.mula.should.be.a.Function;

    mat.mula(-1);
    mat.toString().should.equal("-1 -2 -3\n-4 -5 -6\n-7 -8 -9");
  });

  it('#mul() should return a CMatrix with the product of a matrix and a complex', function() {
    mat.mul.should.be.a.Function;

    var c = new Complex(-1, 0);
    mat.mul(c).toString().should.equal("(-1,0) (-2,0) (-3,0)\n(-4,0) (-5,0) (-6,0)\n(-7,0) (-8,0) (-9,0)");
  });

  it('#div() should return a Matrix which be divied by a scalar value', function() {
    mat.div.should.be.a.Function;

    var mat2 = mat.div(0);
    mat2.equals(
      new Matrix(3, 3)
      .set([
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity
      ])
    ).should.ok;
    mat.div(2).toString().should.equal("0.5   1 1.5\n  2 2.5   3\n3.5   4 4.5");
  });

  it('#div() should return a Matrix which be divied by a complex value', function() {
    mat.div.should.be.a.Function;

    mat.div(Complex(2, 0)).toString().should.equal("(0.5,0)   (1,0) (1.5,0)\n  (2,0) (2.5,0)   (3,0)\n(3.5,0)   (4,0) (4.5,0)");
  });

  it('#diva() should return a Matrix which be divied by a scalar value then saves it back', function() {
    mat.diva.should.be.a.Function;

    mat.diva(0);
    mat.equals(
      new Matrix(3, 3)
      .set([
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity
      ])
    ).should.ok;
    mat.get(0, 0).should.be.a.Infinity;
  });

  it('#equals() should return true if two Matrix are equal', function() {
    mat.equals.should.be.a.Function;

    mat.equals(mat).should.ok;
    mat.equals(new Matrix(3, 3).set([
      1, 2, 3,
      4, 5, 6,
      7, 8, 9
    ])).should.ok;
  });

  it('#isApprox() should return true if this is approximately equal to other', function() {
    mat.isApprox.should.be.a.Function;

    var mat2 = new Matrix(3, 3).set([
      0.111, 0.222, 0.333,
      0.444, 0.555, 0.666,
      0.777, 0.888, 0.999
    ]);
    mat.div(9).isApprox(mat2, 1e-3).should.false;
    mat.div(9).isApprox(mat2, 1e-2).should.true;
  });

  it('#Identity() should return a identity matrix', function() {
    Matrix.Identity.should.be.a.Function;

    Matrix.Identity(0).toString().should.equal("");

    mat = Matrix.Identity(3);
    mat.equals(new Matrix(3, 3).set([
     1, 0, 0,
     0, 1, 0,
     0, 0, 1
    ])).should.true;

    mat = Matrix.Identity(3, 4);
    mat.equals(new Matrix(3, 4).set([
     1, 0, 0, 0,
     0, 1, 0, 0,
     0, 0, 1, 0
    ])).should.true;
  });
});
<commit_msg>test: added 'Matrix.Zero()' testing<commit_after>const
    Matrix = require('../index.js').Matrix,
    Complex = require('../index.js').Complex,
    CMatrix = require('../index.js').CMatrix,
    should = require('should');

describe('Matrix', function() {
  var mat;

  it('#Matrix() should be a function', function() {
    Matrix.should.be.a.Function;
  });

  it('should throw error when tried creating matrix without rows and columns arguments', function() {
    (function() { new Matrix(); }).should.throw('Tried creating matrix without rows and columns arguments');
    (function() { new Matrix(1); }).should.throw('Tried creating matrix without rows and columns arguments');
    (function() { new Matrix(1, 2); }).should.not.throw();
  });

  it('should be invoked with arguments rows and columns and return an object', function() {
    Matrix(1,2).should.be.an.Object;
    (new Matrix(3, 4)).should.be.an.Object;
  });

  it('#Matrix(3, 2) should return the matrix of size 3x2', function() {
    mat = Matrix(3, 2);
    mat.rows().should.equal(3);
    mat.cols().should.equal(2);
  })

  beforeEach(function() {
    mat = Matrix(3, 3)
      .set(0, 0, 1)
      .set(0, 1, 2)
      .set(0, 2, 3)
      .set(1, 0, 4)
      .set(1, 1, 5)
      .set(1, 2, 6)
      .set(2, 0, 7)
      .set(2, 1, 8)
      .set(2, 2, 9);
  });

  it('#set() should throw message when row or column nubers are out of range', function() {
    mat.set.should.be.a.Function;

    (function() {
      mat.set(3, 0, 68);
    }).should.throw('Row or column numbers are out of range');
    (function() {
      mat.set(-1, -2, 500);
    }).should.throw('Row or column numbers are out of range');
  });

  it('#set() with array argument should work ok', function() {
    mat.set.should.be.a.Function;

    Matrix(3, 3).set([
      1, 2, 3,
      4, 5, 6,
      7, 8, 9
    ]).toString().should.eql(mat.toString());

    (function() {
      Matrix(3, 3).set([
        1, 2, 3,
        4, 5, 6
      ]);
    }).should.throw('Too few coefficients passed to Matrix');

    (function() {
      Matrix(3, 3).set([
        1,  2,  3,
        4,  5,  6,
        7,  8,  9,
       10, 11, 12
      ]);
    }).should.throw('Too many coefficients passed to Matrix');
  });

  it('#get() should return the element value of Matrix', function() {
    mat.get.should.be.a.Function;

    mat.get(0, 0).should.equal(1);
    mat.get(0, 1).should.equal(2);
    mat.get(0, 2).should.equal(3);
    mat.get(1, 0).should.equal(4);
    mat.get(1, 1).should.equal(5);
    mat.get(1, 2).should.equal(6);
    mat.get(2, 0).should.equal(7);
    mat.get(2, 1).should.equal(8);
    mat.get(2, 2).should.equal(9);
    (function(){
      mat.get(3, 0);
    }).should.throw('Row or column numbers are out of range');
  });

  it('#toString() should return all element values of Matrix', function() {
    mat.toString.should.be.a.Function;

    mat.toString().should.equal("1 2 3\n4 5 6\n7 8 9");
  });

  it('#add() should return the sum of two matrices', function() {
    mat.add.should.be.a.Function;

    Matrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]).add(mat).toString().should.equal(" 3  6  9\n12 14 16\n18 20 22");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).add(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#add() should return a CMatrix with the sum of a matrix and a complex matrix', function() {
    mat.add.should.be.a.Function;

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    mat.add(cmat).toString().should.equal(" (3,0)  (6,0)  (9,0)\n(12,0) (14,0) (16,0)\n(18,0) (20,0) (22,0)");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).add(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#adda() should return the sum of two matrices and saves it back', function() {
    mat.adda.should.be.a.Function;

    mat.adda(
      Matrix(3, 3)
      .set([
        2,  4,  6,
        8,  9, 10,
       11, 12, 13
      ])
    );
    mat.toString().should.equal(" 3  6  9\n12 14 16\n18 20 22");

    (function() {
      mat.adda(
        Matrix(2, 3)
        .set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    (mat.adda(cmat) == undefined).should.be.true;
  });

  it('#sub() should return the difference of two matrices', function() {
    mat.sub.should.be.a.Function;

    Matrix(3, 3).set([
      1,  3,  5,
      7,  9, 11,
     13, 15, 17
    ]).sub(mat).toString().should.equal("0 1 2\n3 4 5\n6 7 8");

    (function() {
      Matrix(2, 3).set([
        1, 0, 0,
        0, 1, 0
      ]).sub(mat);
    }).should.throw("Nonconformant arguments");
  });

  it('#sub() should return a CMatrix with the sum of a matrix and complex matrix', function() {
    mat.add.should.be.a.Function;

    var cmat = CMatrix(3, 3).set([
      Complex( 2, 0), Complex( 4, 1),  Complex(6, 2),
      Complex( 8, 4), Complex( 9, 5), Complex(10, 6),
      Complex(11, 7), Complex(12, 8), Complex(13, 9)
    ]);

    mat.sub(cmat).toString().should.equal(" (-1,0) (-2,-1) (-3,-2)\n(-4,-4) (-4,-5) (-4,-6)\n(-4,-7) (-4,-8) (-4,-9)");

    (function() {
      mat.sub(
        CMatrix(2, 3).set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");
  });

  it('#suba() should return the difference of two matrices and saves it back', function() {
    mat.suba.should.be.a.Function;

    var mat2 = Matrix(3, 3)
    .set([
      1,  3,  5,
      7,  9, 11,
     13, 15, 17
    ]);
    mat2.suba(mat);
    mat2.toString().should.equal("0 1 2\n3 4 5\n6 7 8");

    (function() {
      mat.suba(
        Matrix(2, 3)
        .set([
          1, 0, 0,
          0, 1, 0
        ])
      );
    }).should.throw("Nonconformant arguments");

    var cmat = CMatrix(3, 3).set([
      2,  4,  6,
      8,  9, 10,
     11, 12, 13
    ]);
    (mat.suba(cmat) == undefined).should.be.true;
  });

  it('#mul() should return the product of two matrices', function() {
    mat.mul.should.be.a.Function;

    var vector = new Matrix(3, 1).set([1, 2, 3]);
    mat.mul(vector).toString().should.equal("14\n32\n50");

    (function() {
      vector.mul(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mul() should return a CMatrix with the product of a matrix and a complex matrix', function() {
    mat.mul.should.be.a.Function;

    var cvector = new CMatrix(3, 1).set([Complex(1, 1), Complex(2, 2), Complex(3, 3)]);
    mat.mul(cvector).toString().should.equal("14\n14\n32");

    (function() {
      cvector.mul(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mul() should return the product of a matrix and a scalar', function() {
    mat.mul.should.be.a.Function;

    mat.mul(-1).toString().should.equal("-1 -2 -3\n-4 -5 -6\n-7 -8 -9");
  });

  it('#mula() should return the product of two matrices and saves it back', function() {
    mat.mula.should.be.a.Function;

    var vector = new Matrix(3, 1).set([1, 2, 3]);
    mat.mula(vector);
    mat.toString().should.equal("14\n32\n50");

    (function() {
      vector.mula(mat);
    }).should.throw("Invalid matrix product");
  });

  it('#mula() should return the product of a matrices and a scalar value then saves it back', function() {
    mat.mula.should.be.a.Function;

    mat.mula(-1);
    mat.toString().should.equal("-1 -2 -3\n-4 -5 -6\n-7 -8 -9");
  });

  it('#mul() should return a CMatrix with the product of a matrix and a complex', function() {
    mat.mul.should.be.a.Function;

    var c = new Complex(-1, 0);
    mat.mul(c).toString().should.equal("(-1,0) (-2,0) (-3,0)\n(-4,0) (-5,0) (-6,0)\n(-7,0) (-8,0) (-9,0)");
  });

  it('#div() should return a Matrix which be divied by a scalar value', function() {
    mat.div.should.be.a.Function;

    var mat2 = mat.div(0);
    mat2.equals(
      new Matrix(3, 3)
      .set([
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity
      ])
    ).should.ok;
    mat.div(2).toString().should.equal("0.5   1 1.5\n  2 2.5   3\n3.5   4 4.5");
  });

  it('#div() should return a Matrix which be divied by a complex value', function() {
    mat.div.should.be.a.Function;

    mat.div(Complex(2, 0)).toString().should.equal("(0.5,0)   (1,0) (1.5,0)\n  (2,0) (2.5,0)   (3,0)\n(3.5,0)   (4,0) (4.5,0)");
  });

  it('#diva() should return a Matrix which be divied by a scalar value then saves it back', function() {
    mat.diva.should.be.a.Function;

    mat.diva(0);
    mat.equals(
      new Matrix(3, 3)
      .set([
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity,
        Infinity, Infinity, Infinity
      ])
    ).should.ok;
    mat.get(0, 0).should.be.a.Infinity;
  });

  it('#equals() should return true if two Matrix are equal', function() {
    mat.equals.should.be.a.Function;

    mat.equals(mat).should.ok;
    mat.equals(new Matrix(3, 3).set([
      1, 2, 3,
      4, 5, 6,
      7, 8, 9
    ])).should.ok;
  });

  it('#isApprox() should return true if this is approximately equal to other', function() {
    mat.isApprox.should.be.a.Function;

    var mat2 = new Matrix(3, 3).set([
      0.111, 0.222, 0.333,
      0.444, 0.555, 0.666,
      0.777, 0.888, 0.999
    ]);
    mat.div(9).isApprox(mat2, 1e-3).should.false;
    mat.div(9).isApprox(mat2, 1e-2).should.true;
  });

  it('#Identity() should return a identity matrix', function() {
    Matrix.Identity.should.be.a.Function;

    Matrix.Identity(0).toString().should.equal("");

    mat = Matrix.Identity(3);
    mat.equals(new Matrix(3, 3).set([
     1, 0, 0,
     0, 1, 0,
     0, 0, 1
    ])).should.true;

    mat = Matrix.Identity(3, 4);
    mat.equals(new Matrix(3, 4).set([
     1, 0, 0, 0,
     0, 1, 0, 0,
     0, 0, 1, 0
    ])).should.true;
  });

  it('#Zero() should return a zero matrix', function() {
    Matrix.Zero.should.be.a.Function;

    Matrix.Zero(3, 3).toString().should.equal("0 0 0\n0 0 0\n0 0 0");

    Matrix.Zero(3, 4).equals(
      new Matrix(3, 4).set([
        0, 0, 0, 0,
        0, 0, 0, 0,
        0, 0, 0, 0
      ])
    ).should.true;
  });
});
<|endoftext|>


<commit_before>/* global describe, it */

import assert from 'assert'
import child_process from 'child_process'
import sinon from 'sinon'
import Runner from '../src/Runner'

const hook = 'pre-commit'
const config = './test/fixtures/configs/package.json'

describe('Runner', () => {
  it('is a class', done => {
    const runner = new Runner(hook, config)

    assert.strictEqual(typeof Runner, 'function')
    assert.strictEqual(runner.constructor, Runner)

    done()
  })

  it('requires a hook', done => {
    assert.throws(() => {
      return new Runner()
    }, /Missing required/)

    done()
  })

  it('validates its hook', done => {
    ['precommit', 'pre_commit', 'Commit'].map(hook => {
      assert.throws(() => {
        return new Runner(hook, config)
      }, /not valid hook name/)
    })

    done()
  })

  describe('#hook', () => {
    it('holds the target hook script name', done => {
      const runner = new Runner(hook, config)

      assert.strictEqual(runner.hook, hook)

      done()
    })
  })

  describe('#commands', () => {
    it('can hold a single executable command', done => {
      const runner = new Runner(hook, config)

      const commands = [
        [ 'npm', [ 'test', '-s' ] ]
      ]

      assert.deepEqual(runner.commands, commands)

      done()
    })

    it('can hold multiple executable command', done => {
      const runner = new Runner(hook, './test/fixtures/configs/multiple-commands.json')

      const commands = [
        [ 'npm', [ 'test', '-s' ] ],
        [ 'npm', [ 'run', 'lint:js', '-s' ] ],
        [ 'npm', [ 'run', 'lint:spaces', '-s' ] ]
      ]

      assert.deepEqual(runner.commands, commands)

      done()
    })

    it('is false when package.json cannot be found', done => {
      const runner = new Runner(hook, './does-not-exist.json')

      assert.strictEqual(runner.commands, false)

      done()
    })

    it('is false when hooks key cannot be found', done => {
      const runner = new Runner(hook, './test/fixtures/configs/missing-hooks.json')

      assert.strictEqual(runner.commands, false)

      done()
    })

    it('is false when hooks key is empty', done => {
      const runner = new Runner('update', './test/fixtures/configs/empty-hooks.json')

      assert.strictEqual(runner.commands, false)

      done()
    })
  })

  describe('#run', () => {
    it('calls child_process.spawn once', done => {
      const stub = sinon.stub(child_process, 'spawn')
      const runner = new Runner(hook, config)

      runner.run()
      assert(stub.calledOnce)

      done()
    })
  })
})
<commit_msg>Build fixture path from a single base path<commit_after>/* global describe, it */

import assert from 'assert'
import child_process from 'child_process'
import sinon from 'sinon'
import Runner from '../src/Runner'

const hook = 'pre-commit'
const basePath = './test/fixtures/configs'
const config = `${basePath}/package.json`

describe('Runner', () => {
  it('is a class', done => {
    const runner = new Runner(hook, config)

    assert.strictEqual(typeof Runner, 'function')
    assert.strictEqual(runner.constructor, Runner)

    done()
  })

  it('requires a hook', done => {
    assert.throws(() => {
      return new Runner()
    }, /Missing required/)

    done()
  })

  it('validates its hook', done => {
    ['precommit', 'pre_commit', 'Commit'].map(hook => {
      assert.throws(() => {
        return new Runner(hook, config)
      }, /not valid hook name/)
    })

    done()
  })

  describe('#hook', () => {
    it('holds the target hook script name', done => {
      const runner = new Runner(hook, config)

      assert.strictEqual(runner.hook, hook)

      done()
    })
  })

  describe('#commands', () => {
    it('can hold a single executable command', done => {
      const runner = new Runner(hook, config)

      const commands = [
        [ 'npm', [ 'test', '-s' ] ]
      ]

      assert.deepEqual(runner.commands, commands)

      done()
    })

    it('can hold multiple executable command', done => {
      const runner = new Runner(hook, `${basePath}/multiple-commands.json`)

      const commands = [
        [ 'npm', [ 'test', '-s' ] ],
        [ 'npm', [ 'run', 'lint:js', '-s' ] ],
        [ 'npm', [ 'run', 'lint:spaces', '-s' ] ]
      ]

      assert.deepEqual(runner.commands, commands)

      done()
    })

    it('is false when package.json cannot be found', done => {
      const runner = new Runner(hook, './does-not-exist.json')

      assert.strictEqual(runner.commands, false)

      done()
    })

    it('is false when hooks key cannot be found', done => {
      const runner = new Runner(hook, `${basePath}/missing-hooks.json`)

      assert.strictEqual(runner.commands, false)

      done()
    })

    it('is false when hooks key is empty', done => {
      const runner = new Runner('update', `${basePath}/empty-hooks.json`)

      assert.strictEqual(runner.commands, false)

      done()
    })
  })

  describe('#run', () => {
    it('calls child_process.spawn once', done => {
      const stub = sinon.stub(child_process, 'spawn')
      const runner = new Runner(hook, config)

      runner.run()
      assert(stub.calledOnce)

      done()
    })
  })
})
<|endoftext|>


<commit_before>import assert from 'power-assert'
import sinon from 'sinon'
import {Translation} from '../src/scene/translation'
import {makeViewport} from '../src/scene/viewport'
import {handle} from '../src/event'

suite('Avatar')

var avatars
var context
var state
beforeEach(function() {
    state = {skid: {}}
    context = {}
    var canvas = {
        getContext: function() {
            return context
        }
    }
    avatars = makeViewport(state, canvas)
})

test('does not sort new avatars', function() {
    var a = new Translation(state, avatars)
    assert(a.layer === undefined)

    a.layer = 10
    assert(a.layer === 10)

    var b = new Translation(state, avatars)
    assert(b.layer === undefined)
    assert(a.layer === 10)
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
})

test('sorts avatars ascendingly', function() {
    var a = new Translation(state, avatars)
    a.layer = 10

    var b = new Translation(state, avatars)
    b.layer = 2

    assert(avatars.contents[0] === b)
    assert(avatars.contents[1] === a)
    assert(avatars.contents.length === 2)

    b.layer = 11
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
    assert(avatars.contents.length === 2)
})

test('sorts past avatars of undefined layer', function() {
    var a = new Translation(state, avatars)
    a.layer = 9
    var b = new Translation(state, avatars)
    var c = new Translation(state, avatars)
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
    assert(avatars.contents[2] === c)

    c.layer = 7
    assert(avatars.contents[0] === c)
    assert(avatars.contents[1] === a)
    assert(avatars.contents[2] === b)
})

test('sorts many avatars', function() {
    var a = new Translation(state, avatars)
    a.layer = 1.001
    var b = new Translation(state, avatars)
    b.layer = 999
    var c = new Translation(state, avatars)
    c.layer = 1.001
    var d = new Translation(state, avatars)
    d.layer = 1
    var e = new Translation(state, avatars)
    e.layer = 5
    var f = new Translation(state, avatars)
    f.layer = -2
    var g = new Translation(state, avatars)
    g.layer = -2.1
    var h = new Translation(state, avatars)
    var i = new Translation(state, avatars)
    i.layer = -1
    h.layer = 4

    assert(avatars.contents[0] === g)
    assert(avatars.contents[1] === f)
    assert(avatars.contents[2] === i)
    assert(avatars.contents[3] === d)
    assert(avatars.contents[4] === a || avatars.contents[4] === c)
    assert(avatars.contents[5] === c || avatars.contents[5] === a)
    assert(avatars.contents[6] === h)
    assert(avatars.contents[7] === e)
    assert(avatars.contents[8] === b)

    a.layer = 4
    b.layer = 7
    c.layer = -.01
    d.layer = -1
    e.layer = 3.999
    f.layer = 3.9999999
    g.layer = 8
    h.layer = 0
    i.layer = 4

    assert(avatars.contents[0] === d)
    assert(avatars.contents[1] === c)
    assert(avatars.contents[2] === h)
    assert(avatars.contents[3] === e)
    assert(avatars.contents[4] === f)
    assert(avatars.contents[5] === i)
    assert(avatars.contents[6] === a)
    assert(avatars.contents[7] === b)
    assert(avatars.contents[8] === g)
})

test('draws all avatars in order', function() {
    var i = 0

    var a = new Translation(state, avatars)
    a.layer = 4
    var b = new Translation(state, avatars)
    b.layer = 3
    a.draw = sinon.spy(function(contextArg) {
        assert(contextArg === context)
        assert(i === 1)
        i += 1
    })
    b.draw = sinon.spy(function(contextArg) {
        assert(contextArg === context)
        assert(i === 0)
        i += 1
    })

    assert(avatars.contents[0] === b)
    assert(avatars.contents[1] === a)

    avatars.draw(context)
    assert(a.draw.called)
    assert(b.draw.called)
})

test('leaves no interpolands behind after removal', function() {
    const av = new Translation(state, avatars)
    av.remove()
    assert(av.removed)
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)

    // double-removal should have no effect
    av.remove()
    assert(av.removed)
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)
})

test('leaves no interpolands behind after multi-removal and calls all onRemoves', function() {
    var a = new Translation(state, avatars)
    var b = new Translation(state, avatars)
    var c = new Translation(state, avatars)

    a.remove()
    b.remove()
    c.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)
})

test('handles multiple creations and removals', function() {
    var a = new Translation(state, avatars)
    var b = new Translation(state, avatars)
    var c = new Translation(state, avatars)
    var d = new Translation(state, avatars)
    var e = new Translation(state, avatars)

    assert(avatars.contents.length === 5)
    assert(state.skid.interpolands.interpolands.length === 10)

    d.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
    assert(avatars.contents[2] === c)
    assert(avatars.contents[3] === e)

    assert(avatars.contents.length === 4)
    assert(state.skid.interpolands.interpolands.length === 8)

    var dx = d
    d = new Translation(state, avatars)
    assert(d !== dx)
    assert(avatars.contents[3] === e)
    assert(avatars.contents[4] === d)

    a.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 4)
    assert(state.skid.interpolands.interpolands.length === 8)
    assert(avatars.contents[0] === b)
    assert(avatars.contents[1] === c)
    assert(avatars.contents[2] === e)
    assert(avatars.contents[3] === d)

    var ax = a
    a = new Translation(state, avatars)
    assert(a !== ax)
    assert(avatars.contents.length === 5)
    assert(state.skid.interpolands.interpolands.length === 10)
    assert(avatars.contents[4] === a)

    c.remove()
    b.remove()
    e.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 2)
    assert(state.skid.interpolands.interpolands.length === 4)
    assert(avatars.contents[0] === d)
    assert(avatars.contents[1] === a)

    e = new Translation(state, avatars)
    b = new Translation(state, avatars)
    c = new Translation(state, avatars)

    var f = new Translation(state, avatars)
    assert(avatars.contents.length === 6)
    assert(state.skid.interpolands.interpolands.length === 12)
    assert(avatars.contents[0] === d)
    assert(avatars.contents[1] === a)
    assert(avatars.contents[2] === e)
    assert(avatars.contents[3] === b)
    assert(avatars.contents[4] === c)
    assert(avatars.contents[5] === f)
})
<commit_msg>Updated tests<commit_after>import assert from 'power-assert'
import sinon from 'sinon'
import {Translation} from '../src/scene/translation'
import {makeViewport} from '../src/scene/viewport'
import {handle} from '../src/event'

suite('Avatar')

var avatars
var context
var state
beforeEach(function() {
    state = {skid: {}}
    context = {}
    var canvas = {
        getContext: function() {
            return context
        }
    }
    avatars = makeViewport(state, canvas)
})

test('sorts avatars ascendingly', function() {
    var a = new Translation(state, avatars)
    a.layer = 10

    var b = new Translation(state, avatars)
    b.layer = 2

    assert(avatars.contents[0] === b)
    assert(avatars.contents[1] === a)
    assert(avatars.contents.length === 2)

    b.layer = 11
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
    assert(avatars.contents.length === 2)
})

test('sorts many avatars', function() {
    var a = new Translation(state, avatars)
    a.layer = 1.001
    var b = new Translation(state, avatars)
    b.layer = 999
    var c = new Translation(state, avatars)
    c.layer = 1.001
    var d = new Translation(state, avatars)
    d.layer = 1
    var e = new Translation(state, avatars)
    e.layer = 5
    var f = new Translation(state, avatars)
    f.layer = -2
    var g = new Translation(state, avatars)
    g.layer = -2.1
    var h = new Translation(state, avatars)
    var i = new Translation(state, avatars)
    i.layer = -1
    h.layer = 4

    assert(avatars.contents[0] === g)
    assert(avatars.contents[1] === f)
    assert(avatars.contents[2] === i)
    assert(avatars.contents[3] === d)
    assert(avatars.contents[4] === a || avatars.contents[4] === c)
    assert(avatars.contents[5] === c || avatars.contents[5] === a)
    assert(avatars.contents[6] === h)
    assert(avatars.contents[7] === e)
    assert(avatars.contents[8] === b)

    a.layer = 4
    b.layer = 7
    c.layer = -.01
    d.layer = -1
    e.layer = 3.999
    f.layer = 3.9999999
    g.layer = 8
    h.layer = 0
    i.layer = 4

    assert(avatars.contents[0] === d)
    assert(avatars.contents[1] === c)
    assert(avatars.contents[2] === h)
    assert(avatars.contents[3] === e)
    assert(avatars.contents[4] === f)
    assert(avatars.contents[5] === i)
    assert(avatars.contents[6] === a)
    assert(avatars.contents[7] === b)
    assert(avatars.contents[8] === g)
})

test('draws all avatars in order', function() {
    var i = 0

    var a = new Translation(state, avatars)
    a.layer = 4
    var b = new Translation(state, avatars)
    b.layer = 3
    a.draw = sinon.spy(function(contextArg) {
        assert(contextArg === context)
        assert(i === 1)
        i += 1
    })
    b.draw = sinon.spy(function(contextArg) {
        assert(contextArg === context)
        assert(i === 0)
        i += 1
    })

    assert(avatars.contents[0] === b)
    assert(avatars.contents[1] === a)

    avatars.draw(context)
    assert(a.draw.called)
    assert(b.draw.called)
})

test('leaves no interpolands behind after removal', function() {
    const av = new Translation(state, avatars)
    av.remove()
    assert(av.removed)
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)

    // double-removal should have no effect
    av.remove()
    assert(av.removed)
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)
})

test('leaves no interpolands behind after multi-removal and calls all onRemoves', function() {
    var a = new Translation(state, avatars)
    var b = new Translation(state, avatars)
    var c = new Translation(state, avatars)

    a.remove()
    b.remove()
    c.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 0)
    assert(state.skid.interpolands.interpolands.length === 0)
})

test('handles multiple creations and removals', function() {
    var e = new Translation(state, avatars)
    var d = new Translation(state, avatars)
    var c = new Translation(state, avatars)
    var b = new Translation(state, avatars)
    var a = new Translation(state, avatars)

    assert(avatars.contents.length === 5)
    assert(state.skid.interpolands.interpolands.length === 10)

    d.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === b)
    assert(avatars.contents[2] === c)
    assert(avatars.contents[3] === e)

    assert(avatars.contents.length === 4)
    assert(state.skid.interpolands.interpolands.length === 8)

    var dx = d
    d = new Translation(state, avatars)
    assert(d !== dx)
    assert(avatars.contents[1] === a)
    assert(avatars.contents[0] === d)

    a.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 4)
    assert(state.skid.interpolands.interpolands.length === 8)
    assert(avatars.contents[0] === d)
    assert(avatars.contents[1] === b)
    assert(avatars.contents[2] === c)
    assert(avatars.contents[3] === e)

    var ax = a
    a = new Translation(state, avatars)
    assert(a !== ax)
    assert(avatars.contents.length === 5)
    assert(state.skid.interpolands.interpolands.length === 10)
    assert(avatars.contents[0] === a)

    c.remove()
    b.remove()
    e.remove()
    handle(state, 'before_draw', 0)
    assert(avatars.contents.length === 2)
    assert(state.skid.interpolands.interpolands.length === 4)
    assert(avatars.contents[0] === a)
    assert(avatars.contents[1] === d)

    e = new Translation(state, avatars)
    b = new Translation(state, avatars)
    c = new Translation(state, avatars)

    var f = new Translation(state, avatars)
    assert(avatars.contents.length === 6)
    assert(state.skid.interpolands.interpolands.length === 12)
    assert(avatars.contents[0] === f)
    assert(avatars.contents[1] === c)
    assert(avatars.contents[2] === b)
    assert(avatars.contents[3] === e)
    assert(avatars.contents[4] === a)
    assert(avatars.contents[5] === d)
})
<|endoftext|>


<commit_before>// This example uses the Phaser 2.2.2 framework

// Copyright © 2014 John Watson
// Licensed under the terms of the MIT License

var GameState = function(game) {
};

// Load images and sounds
GameState.prototype.preload = function() {
    this.game.load.spritesheet('ship', '/assets/gfx/ship.png', 32, 32);
    this.game.load.image('ground', '/assets/gfx/ground.png');
    this.game.load.spritesheet('explosion', '/assets/gfx/explosion.png', 128, 128);
};

// Setup the example
GameState.prototype.create = function() {
    // Set stage background color
    this.game.stage.backgroundColor = 0x333333;

    // Define motion constants
    // this.ROTATION_SPEED = 180; // degrees/second
    // this.ACCELERATION = 200; // pixels/second/second
    // this.MAX_SPEED = 250; // pixels/second
    // this.DRAG = 0; // pixels/second
    this.GRAVITY = 400; // pixels/second/second

    // Add the ship to the stage
    this.ship = this.game.add.sprite(0, 0, 'ship');
    this.ship.anchor.setTo(0.5, 0.5);
    this.ship.angle = 0; // Point the ship up

    // Enable physics on the ship
    this.game.physics.enable(this.ship, Phaser.Physics.ARCADE);

    this.ship.gravity = 400;

    // // Set maximum velocity
    // this.ship.body.maxVelocity.setTo(this.MAX_SPEED, this.MAX_SPEED); // x, y

    // Add drag to the ship that slows it down when it is not accelerating
    this.ship.body.drag.setTo(this.DRAG, this.DRAG); // x, y

    // Choose a random starting angle and velocity for the ship
    this.resetShip();

    // Turn on gravity
    game.physics.arcade.gravity.y = this.GRAVITY;

    // Make ship bounce a little
    this.ship.body.bounce.setTo(0.25, 0.25);

    // Create some ground for the ship to land on
    game.time.events.loop(1000, send, this);
    this.ground = this.game.add.group();
    function send(){
    // this.ground = this.game.add.group();
    for (x = 0; x < this.game.rnd.integerInRange(1, 3); x += 1) {
        var randomValue = this.game.rnd.integerInRange(32, 300);
        // Add the ground blocks, enable physics on each, make them immovable
        var groundBlock = this.game.add.sprite(game.width + 32, randomValue, 'ground');
        this.game.physics.enable(groundBlock, Phaser.Physics.ARCADE);
        groundBlock.body.immovable = true;
        groundBlock.body.allowGravity = false;
        groundBlock.body.velocity.x = -300;
        this.ground.add(groundBlock);
    }
  }

    // Create a group for explosions
    this.explosionGroup = this.game.add.group();

    // Capture certain keys to prevent their default actions in the browser.
    // This is only necessary because this is an HTML5 game. Games on other
    // platforms may not need code like this.
    this.game.input.keyboard.addKeyCapture([
        Phaser.Keyboard.LEFT,
        Phaser.Keyboard.RIGHT,
        Phaser.Keyboard.UP,
        Phaser.Keyboard.DOWN
    ]);
};

// Try to get a used explosion from the explosionGroup.
// If an explosion isn't available, create a new one and add it to the group.
// Setup new explosions so that they animate and kill themselves when the
// animation is complete.
GameState.prototype.getExplosion = function(x, y) {
    // Get the first dead explosion from the explosionGroup
    var explosion = this.explosionGroup.getFirstDead();

    // If there aren't any available, create a new one
    if (explosion === null) {
        explosion = this.game.add.sprite(0, 0, 'explosion');
        explosion.anchor.setTo(0.5, 0.5);

        // Add an animation for the explosion that kills the sprite when the
        // animation is complete
        var animation = explosion.animations.add('boom', [0,1,2,3], 60, false);
        animation.killOnComplete = true;

        // Add the explosion sprite to the group
        this.explosionGroup.add(explosion);
    }

    // Revive the explosion (set it's alive property to true)
    // You can also define a onRevived event handler in your explosion objects
    // to do stuff when they are revived.
    explosion.revive();

    // Move the explosion to the given coordinates
    explosion.x = x;
    explosion.y = y;

    // Set rotation of the explosion at random for a little variety
    explosion.angle = this.game.rnd.integerInRange(0, 360);

    // Play the animation
    explosion.animations.play('boom');

    // Return the explosion itself in case we want to do anything else with it
    return explosion;
};

GameState.prototype.resetShip = function() {
    // Move the ship back to the top of the stage
    this.ship.x = 32;
    this.ship.y = 32;
    this.ship.body.acceleration.setTo(0, 0);
    this.ship.body.velocity.setTo(0,0)

};

// The update() method is called every frame
GameState.prototype.update = function() {

    // Collide the ship with the ground
    if(this.game.physics.arcade.collide(this.ship, this.ground)){
            this.getExplosion(this.ship.x, this.ship.y);
            this.resetShip()
          }

    if (this.upInputIsActive()) {
        // If the UP key is down, thrust
        // Calculate acceleration vector based on this.angle and this.ACCELERATION
        // this.ship.body.acceleration.x = Math.cos(this.ship.rotation) * this.ACCELERATION;
        // this.ship.body.acceleration.y = Math.sin(this.ship.rotation) * this.ACCELERATION;
        this.ship.body.velocity.y = -120;
        // Show the frame from the spritesheet with the engine on
        this.ship.frame = 1;
    } else {
        // Otherwise, stop thrusting
        // this.ship.body.acceleration.setTo(0, 0);

        // Show the frame from the spritesheet with the engine off
        this.ship.frame = 0;
    }
};

GameState.prototype.upInputIsActive = function() {
    var isActive = false;

    isActive = this.input.keyboard.isDown(Phaser.Keyboard.UP);
    isActive |= (this.game.input.activePointer.isDown &&
        this.game.input.activePointer.x > this.game.width/4 &&
        this.game.input.activePointer.x < this.game.width/2 + this.game.width/4);

    return isActive;
};

var game = new Phaser.Game(1000, 300, Phaser.AUTO, 'game');
game.state.add('game', GameState, true);
<commit_msg>top and bottom borders explode you/<commit_after>// This example uses the Phaser 2.2.2 framework

// Copyright © 2014 John Watson
// Licensed under the terms of the MIT License

var GameState = function(game) {
};

// Load images and sounds
GameState.prototype.preload = function() {
    this.game.load.spritesheet('ship', '/assets/gfx/ship.png', 32, 32);
    this.game.load.image('ground', '/assets/gfx/ground.png');
    this.game.load.spritesheet('explosion', '/assets/gfx/explosion.png', 128, 128);
};

// Setup the example
GameState.prototype.create = function() {
    // Set stage background color
    this.game.stage.backgroundColor = 0x333333;

    // Define motion constants
    // this.ROTATION_SPEED = 180; // degrees/second
    // this.ACCELERATION = 200; // pixels/second/second
    // this.MAX_SPEED = 250; // pixels/second
    // this.DRAG = 0; // pixels/second
    this.GRAVITY = 400; // pixels/second/second

    // Add the ship to the stage
    this.ship = this.game.add.sprite(0, 0, 'ship');
    this.ship.anchor.setTo(0.5, 0.5);
    this.ship.angle = 0; // Point the ship up

    // Enable physics on the ship
    this.game.physics.enable(this.ship, Phaser.Physics.ARCADE);

    this.ship.body.collideWorldBounds = true;

    this.ship.gravity = 400;

    // // Set maximum velocity
    // this.ship.body.maxVelocity.setTo(this.MAX_SPEED, this.MAX_SPEED); // x, y

    // Add drag to the ship that slows it down when it is not accelerating
    this.ship.body.drag.setTo(this.DRAG, this.DRAG); // x, y

    // Choose a random starting angle and velocity for the ship
    this.resetShip();

    // Turn on gravity
    game.physics.arcade.gravity.y = this.GRAVITY;

    // Make ship bounce a little
    this.ship.body.bounce.setTo(0.25, 0.25);

    // Create some ground for the ship to land on
    game.time.events.loop(800, send, this);
    this.ground = this.game.add.group();
    function send(){
    // this.ground = this.game.add.group();
    for (x = 0; x < this.game.rnd.integerInRange(1, 3); x += 1) {
        var randomValue = this.game.rnd.integerInRange(32, 300);
        // Add the ground blocks, enable physics on each, make them immovable
        var groundBlock = this.game.add.sprite(game.width + 32, randomValue, 'ground');
        this.game.physics.enable(groundBlock, Phaser.Physics.ARCADE);
        groundBlock.body.immovable = true;
        groundBlock.body.allowGravity = false;
        groundBlock.body.velocity.x = -300;
        this.ground.add(groundBlock);
    }
  }
//make top and bottom borders
      for(var x = 0; x < this.game.width; x += 32) {
        // Add the ground blocks, enable physics on each, make them immovable
        var groundBlock = this.game.add.sprite(x, this.game.height - 32, 'ground');
        var topBlock = this.game.add.sprite(x, this.game.height - 400, 'ground');
        this.game.physics.enable(topBlock, Phaser.Physics.ARCADE);
        topBlock.body.immovable = true;
        topBlock.body.allowGravity = false;
        this.ground.add(topBlock);
        this.game.physics.enable(groundBlock, Phaser.Physics.ARCADE);
        groundBlock.body.immovable = true;
        groundBlock.body.allowGravity = false;
        this.ground.add(groundBlock);
    }

    // Create a group for explosions
    this.explosionGroup = this.game.add.group();

    // Capture certain keys to prevent their default actions in the browser.
    // This is only necessary because this is an HTML5 game. Games on other
    // platforms may not need code like this.
    this.game.input.keyboard.addKeyCapture([
        Phaser.Keyboard.LEFT,
        Phaser.Keyboard.RIGHT,
        Phaser.Keyboard.UP,
        Phaser.Keyboard.DOWN
    ]);
};

// Try to get a used explosion from the explosionGroup.
// If an explosion isn't available, create a new one and add it to the group.
// Setup new explosions so that they animate and kill themselves when the
// animation is complete.
GameState.prototype.getExplosion = function(x, y) {
    // Get the first dead explosion from the explosionGroup
    var explosion = this.explosionGroup.getFirstDead();

    // If there aren't any available, create a new one
    if (explosion === null) {
        explosion = this.game.add.sprite(0, 0, 'explosion');
        explosion.anchor.setTo(0.5, 0.5);

        // Add an animation for the explosion that kills the sprite when the
        // animation is complete
        var animation = explosion.animations.add('boom', [0,1,2,3], 60, false);
        animation.killOnComplete = true;

        // Add the explosion sprite to the group
        this.explosionGroup.add(explosion);
    }

    // Revive the explosion (set it's alive property to true)
    // You can also define a onRevived event handler in your explosion objects
    // to do stuff when they are revived.
    explosion.revive();

    // Move the explosion to the given coordinates
    explosion.x = x;
    explosion.y = y;

    // Set rotation of the explosion at random for a little variety
    explosion.angle = this.game.rnd.integerInRange(0, 360);

    // Play the animation
    explosion.animations.play('boom');

    // Return the explosion itself in case we want to do anything else with it
    return explosion;
};

GameState.prototype.resetShip = function() {
    // Move the ship back to the top of the stage
    this.ship.x = 32;
    this.ship.y = 32;
    this.ship.body.acceleration.setTo(0, 0);
    this.ship.body.velocity.setTo(0,0)

};

// The update() method is called every frame
GameState.prototype.update = function() {

    // Collide the ship with the ground
    if(this.game.physics.arcade.collide(this.ship, this.ground)){
            this.getExplosion(this.ship.x, this.ship.y);
            this.resetShip()
          }

    if (this.upInputIsActive()) {
        // If the UP key is down, thrust
        // Calculate acceleration vector based on this.angle and this.ACCELERATION
        // this.ship.body.acceleration.x = Math.cos(this.ship.rotation) * this.ACCELERATION;
        // this.ship.body.acceleration.y = Math.sin(this.ship.rotation) * this.ACCELERATION;
        this.ship.body.velocity.y = -120;
        // Show the frame from the spritesheet with the engine on
        this.ship.frame = 1;
    } else {
        // Otherwise, stop thrusting
        // this.ship.body.acceleration.setTo(0, 0);

        // Show the frame from the spritesheet with the engine off
        this.ship.frame = 0;
    }
};

GameState.prototype.upInputIsActive = function() {
    var isActive = false;

    isActive = this.input.keyboard.isDown(Phaser.Keyboard.UP);
    isActive |= (this.game.input.activePointer.isDown &&
        this.game.input.activePointer.x > this.game.width/4 &&
        this.game.input.activePointer.x < this.game.width/2 + this.game.width/4);

    return isActive;
};

var game = new Phaser.Game(1000, 400, Phaser.AUTO, 'game');
game.state.add('game', GameState, true);
<|endoftext|>