/*
  procball

  This is a zero player simple and fun simulator of football matches with
  procedurally generated players and teams from many countries. Ideal for
  watching football when you don't have TV or Internet at hand :) You can run
  your own championship whenever and wherever you want. There are over 4 billion
  unique matches, each one having with ID by which you can replay the match at
  any time, or share it with a friend. Players are procedurally generated based
  on their country, each one having many attributes including name, height,
  weight, preferred foot, speed, strength, intelligence etc. Dislike what
  players you got on your favorite team? Just change season -- different ones
  will be generated. But please keep in mind that the game is minimalist,
  simplified (no offside, no cards, ...) and aimed at fun, not realism.

  By drummyfish, released under CC0 1.0, public domain.
*/

#define SAF_PROGRAM_NAME "procball"

#define DEBUG 0
#define SAF_SETTING_FORCE_1BIT 0   // set to 1 for 1bit version

#if DEBUG
  #define DEBUG_PRINT(s) puts(s)
#else
  #define DEBUG_PRINT(s) ;
#endif

#define SAF_SETTING_FASTER_1BIT 1
#define SAF_SETTING_1BIT_DITHER 0
#define SAF_SETTING_ENABLE_SAVES 0

#include "../saf.h"

//====== static data ======

static const uint8_t imageBall[11] =
{
#if SAF_PLATFORM_COLOR_COUNT > 2
  0x03,0x03,0xb7,0xff,0xb7,0xff,0xff,0xff,0xb7,0xff,0xb7
#else
  0x03,0x03,0x00,0x00,0x00,0x00,0xff,0x00,0x00,0x00,0x00
#endif
};

static const uint8_t imageBallShadow[11] =
{
#if SAF_PLATFORM_COLOR_COUNT > 2
  0x03,0x03,0x52,0x4d,0x52,0x4d,0x4d,0x4d,0x52,0x4d,0x52
#else
  0x03,0x03,0xff,0xff,0xff,0x00,0x00,0x00,0x00,0x00,0x00
#endif
};

static const uint8_t imageGrass[66] =
{
  0x08,0x08,0x75,0x75,0x75,0x75,0x75,0x96,0x75,0x75,0x96,0x75,0x75,0x95,0x75,
  0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x95,0x75,0x75,0x75,0x95,0x75,
  0x75,0x75,0x75,0x96,0x75,0x75,0x75,0x96,0x75,0x75,0x75,0x75,0x95,0x75,0x75,
  0x75,0x75,0x95,0x75,0x75,0x75,0x75,0x95,0x75,0x75,0x75,0x96,0x75,0x96,0x75,
  0x75,0x96,0x75,0x75,0x75,0x75
};

static const uint8_t imageGoal[194] =
{
  0x08,0x18,0xdb,0xff,0xff,0xb7,0x75,0x75,0x75,0x75,0xff,0xb6,0xfb,0xff,0xb7,
  0x75,0x75,0x75,0xff,0xfb,0x75,0xd7,0x75,0xb7,0x75,0x75,0xff,0xb6,0xfb,0xff,
  0xbb,0x75,0xb7,0x75,0xff,0xfb,0x75,0xd7,0x75,0xbb,0x50,0xb7,0xff,0xb6,0xfb,
  0xff,0xbb,0x75,0xbb,0xb7,0xff,0xfb,0x75,0xd7,0x75,0xbb,0x50,0xbb,0xff,0xb6,
  0xfb,0xff,0xbb,0xdb,0xbb,0xb7,0xff,0xfb,0x75,0xd7,0x75,0xbb,0x50,0xbb,0xff,
  0xb6,0xfb,0xff,0xbb,0x75,0xbb,0xb7,0xff,0xfb,0x75,0xd7,0x75,0xbb,0x50,0xbb,
  0xff,0xb6,0xfb,0xff,0xbb,0x75,0xbb,0xb7,0xff,0xfb,0x75,0xd7,0x75,0xbb,0x50,
  0xbb,0xff,0xb6,0xfb,0xff,0xbb,0x75,0xbb,0xb7,0xff,0xfb,0x75,0xd7,0x75,0xbb,
  0x50,0xbb,0xff,0xb6,0xfb,0xff,0xbb,0x75,0xbb,0xb7,0xdb,0xff,0xff,0xff,0x75,
  0xbb,0x50,0xbb,0xdb,0xb6,0xb7,0x75,0xdb,0x50,0xbb,0xb7,0xdb,0xb6,0x75,0xb7,
  0x75,0xdb,0x50,0xbb,0xdb,0xb6,0xb7,0x75,0xb7,0x50,0xdb,0xb7,0xdb,0xb6,0x75,
  0xb7,0x50,0xb7,0x50,0xdb,0xdb,0xb6,0xb7,0x75,0xb7,0x50,0xb7,0xdb,0xdb,0xb6,
  0x75,0xb7,0x50,0xb7,0x50,0xdb,0xdb,0xdb,0xdb,0xdb,0xdb,0xdb,0xdb,0xdb
};

#define PLAYER_IMAGE_W 5
#define PLAYER_IMAGE_H 8

static const uint8_t playerImages[] =
{
  0x75,0x44,0x44,0x44,0x75,0x75,0x44,0xf6,0x44,0x75,0xa4,0xa4,0xa4,0xa4,0xa4,
  0xf6,0xa4,0xa4,0xa4,0xf6,0x75,0xdb,0xdb,0xdb,0x75,0x52,0xdb,0x4d,0xdb,0x52,
  0x4d,0x65,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75, // down

  0x75,0x44,0x44,0x44,0x75,0x75,0x44,0xf6,0x44,0xa4,0xa4,0xa4,0xa4,0xa4,0xa4,
  0xf6,0xa4,0xa4,0xa4,0x75,0x75,0xb6,0xdb,0xdb,0x75,0x52,0x4d,0xb6,0xdb,0x52,
  0x4d,0x4d,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75, // down running (L foot)

  0x75,0x44,0x44,0x44,0x75,0x75,0x44,0x44,0x44,0x75,0xa4,0xa4,0xa4,0xa4,0xa4,
  0xf6,0xa4,0xa4,0xa4,0xf6,0x75,0xdb,0xdb,0xdb,0x75,0x52,0xdb,0x4d,0xdb,0x52,
  0x4d,0x65,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75, // up

  0x75,0x44,0x44,0x44,0x75,0x75,0x44,0x44,0x44,0xa4,0xa4,0xa4,0xa4,0xa4,0xa4,
  0xf6,0xa4,0xa4,0xa4,0x75,0x75,0xdb,0xb6,0xdb,0x75,0x52,0x4d,0x4d,0xdb,0x52,
  0x4d,0x4d,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75, // up running (L foot)

  0x75,0x44,0x44,0x75,0x75,0x75,0xf6,0x44,0x75,0x75,0x75,0xa4,0xa4,0x75,0x75,
  0x75,0xa4,0xa4,0x75,0x75,0x75,0xdb,0xf6,0x75,0x75,0x52,0x65,0xdb,0x52,0x75,
  0x4d,0x4d,0x65,0x4d,0x75,0x75,0x4d,0x4d,0x75,0x75, // left

  0x75,0x44,0x44,0x75,0x75,0x75,0xf6,0x44,0x75,0x75,0x75,0xa4,0xa4,0x75,0x75,
  0xf6,0xa4,0xa4,0x75,0x75,0xdb,0xdb,0xdb,0x75,0x75,0x65,0x4d,0xb6,0xdb,0x75,
  0x4d,0x4d,0x4d,0x65,0x75,0x75,0x4d,0x4d,0x75,0x75, // left running (R foot)

  0x75,0x44,0x44,0x75,0x75,0x75,0xf6,0x44,0x75,0x75,0xf6,0xa4,0xa4,0x75,0x75,
  0x75,0xa4,0xa4,0xa4,0x75,0x75,0xdb,0xdb,0xf6,0x75,0xdb,0xb6,0x4d,0x52,0x75,
  0x65,0x4d,0x4d,0x4d,0x75,0x75,0x4d,0x4d,0x75,0x75, // left running (L foot)

  0x75,0x75,0x75,0x75,0x75,0x75,0x65,0x75,0x65,0x75,0x75,0xdb,0x4d,0xdb,0x75,
  0x75,0xdb,0xdb,0xdb,0x75,0x75,0xa4,0xa4,0xa4,0x75,0xa4,0xa4,0xa4,0xa4,0xa4,
  0xa4,0x44,0x44,0x44,0xa4,0xf6,0x4d,0x44,0x4d,0xf6, // down lying 

  0x75,0x75,0x75,0x75,0x75,0xf6,0x4d,0x44,0x4d,0xf6,0xa4,0x44,0x44,0x44,0xa4,
  0xa4,0xa4,0xa4,0xa4,0xa4,0x4d,0xa4,0xa4,0xa4,0x4d,0x52,0xdb,0xdb,0xdb,0x52,
  0x75,0xdb,0x52,0xdb,0x75,0x75,0x65,0x75,0x65,0x75, // up lying

  0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x75,0x75,0xf6,0x75,0x75,0x75,0x75,
  0xa4,0xb6,0xdb,0x65,0x75,0xa4,0xa4,0xb6,0xdb,0x65,0xa4,0xa4,0xa4,0x4d,0x75,
  0x44,0x44,0xa4,0x75,0x75,0x44,0x44,0xf6,0x75,0x75, // left lying

  0xf6,0x44,0x44,0x44,0xf6,0xa4,0x44,0xf6,0x44,0xa4,0xa4,0xa4,0xa4,0xa4,0xa4,
  0x75,0xa4,0xa4,0xa4,0x75,0x75,0xdb,0xdb,0xdb,0x75,0x52,0xdb,0x4d,0xdb,0x52,
  0x4d,0x65,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75, // down throwing

  0xf6,0x44,0x44,0x44,0xf6,0xa4,0x44,0x44,0x44,0xa4,0xa4,0xa4,0xa4,0xa4,0xa4,
  0x75,0xa4,0xa4,0xa4,0x75,0x75,0xdb,0xdb,0xdb,0x75,0x52,0xdb,0x4d,0xdb,0x52,
  0x4d,0x65,0x4d,0x65,0x4d,0x75,0x4d,0x4d,0x4d,0x75 // up throwing
};

#define SQUARE_SIZE 8                 // small spatial unit
#define PITCH_W (27 * SQUARE_SIZE)
#define PITCH_H (16 * SQUARE_SIZE)

#define CAMERA_SPEED 4                // free camera speed

#define PLAY_FRAMES (SAF_FPS * 600)   // playing time of one game, in frames

#define PLAYERS_IN_TEAM 8
#define PLAYERS_TOTAL (PLAYERS_IN_TEAM * 2 + 1) // including referee
#define NAME_MAX_LEN 7                // player name max len (without term. 0) 

// pitch line dimensions:
#define PENALTY_AREA_W (PITCH_W / 6)
#define PENALTY_AREA_H (4 * PITCH_H / 9)
#define GOAL_AREA_W (PITCH_W / 10)
#define GOAL_AREA_H (PITCH_H / 4)
#define CENTER_CIRCLE_R SQUARE_SIZE

#define DIST_CLOSE (PITCH_H / 3)      // distance considered "close"
#define DIST_AT (SQUARE_SIZE / 2)     // reachable distance

#define BALL_BOUNCE 4                 // higher number => smaller bounce
#define BALL_CATCH_HEIGHT 5           // max height where ball can be caught
#define BALL_HAND_HEIGHT 2            // min height for hand to occur

#define TRIP_TIME (SAF_FPS * 8)       // max time in which someone trips

#define COUNTRIES 32

#define GOAL_WIDTH (SQUARE_SIZE * 2 + SQUARE_SIZE / 2)
#define GOAL_HEIGHT 7

#define COLOR_LINE            0xb7    // pitch line color
#define COLOR_TEAM_A          0xa4
#define COLOR_TEAM_B          0x0f
#define COLOR_REFEREE         0x92
#define COLOR_SKIN_WHITE      0xf6
#define COLOR_SKIN_BLACK      0x68
#define COLOR_HAIR_DARK       0x44
#define COLOR_HAIR_LIGHT      0xac
#define COLOR_SHOES_1         0x65
#define COLOR_SHOES_2         0x0a
#define COLORTEXT_1           SAF_COLOR_GRAY_DARK
#define COLORTEXT_2           SAF_COLOR_WHITE
#define MENU_BACKGROUND_COLOR 0x99

#define SOUND_WHISTLE         SAF_SOUND_CLICK
#define SOUND_BUMP            SAF_SOUND_BUMP
#define SOUND_BOOM            SAF_SOUND_BOOM
#define SOUND_BEEP            SAF_SOUND_BEEP

#define PLAYER_STATE_MASK     0xf0
#define PLAYER_DIR_MASK       0x0f

#define PLAYER_BASE_STATE_FRAMES 32   // base no. of frames for state duration
#define PLAYER_STATE_STANDING    0x00
#define PLAYER_STATE_WALKING     0x10
#define PLAYER_STATE_RUNNING     0x20
#define PLAYER_STATE_LYING       0x30
#define PLAYER_STATE_KICK_R      0x40
#define PLAYER_STATE_KICK_L      0x50
#define PLAYER_STATE_THROWING    0x60

#define PLAYER_DIR_U          0
#define PLAYER_DIR_UR         1
#define PLAYER_DIR_R          2
#define PLAYER_DIR_DR         3
#define PLAYER_DIR_D          4
#define PLAYER_DIR_DL         5
#define PLAYER_DIR_L          6
#define PLAYER_DIR_UL         7

#define RACE_BIT 0x01
#define HAIR_BIT 0x02
#define SHOE_BIT 0x04
#define FOOT_BIT 0x08

#define ATTRIBUTE_MAX 127  // maximum for certain attributes

#define BALL_ANIM_FRAMES 32

#if SAF_PLATFORM_COLOR_COUNT <= 2
  // redefine colors for 1 bit version:
  #undef COLOR_LINE
  #undef COLORTEXT_1
  #undef COLORTEXT_2
  #undef COLOR_TEAM_A
  #undef COLOR_TEAM_B
  #undef COLOR_REFEREE
  #undef COLOR_SKIN_WHITE
  #undef COLOR_SKIN_BLACK
  #undef COLOR_HAIR_DARK
  #undef COLOR_HAIR_LIGHT
  #undef COLOR_SHOES_1
  #undef COLOR_SHOES_2
  #undef MENU_BACKGROUND_COLOR

  #define COLOR_LINE            SAF_COLOR_BLACK
  #define COLORTEXT_1           SAF_COLOR_BLACK 
  #define COLORTEXT_2           SAF_COLOR_BLACK
  #define COLOR_TEAM_A          SAF_COLOR_BLACK
  #define COLOR_TEAM_B          SAF_COLOR_WHITE
  #define COLOR_REFEREE         SAF_COLOR_BLACK
  #define COLOR_SKIN_WHITE      SAF_COLOR_BLACK
  #define COLOR_SKIN_BLACK      SAF_COLOR_BLACK
  #define COLOR_HAIR_DARK       SAF_COLOR_BLACK
  #define COLOR_HAIR_LIGHT      SAF_COLOR_BLACK
  #define COLOR_SHOES_1         SAF_COLOR_BLACK
  #define COLOR_SHOES_2         SAF_COLOR_BLACK
  #define MENU_BACKGROUND_COLOR SAF_COLOR_WHITE
#endif

static const char countryNames[] =
  "AR" /* Argentina */    "AT" /* Austria */   "AU" /* Australia */
  "AQ" /* Antarctica */   "BR" /* Brazil */    "CA" /* Canada */
  "CN" /* China */        "CZ" /* Czechia */   "DE" /* Germany */
  "ES" /* Spain */        "FI" /* Finland */   "FR" /* France */
  "GB" /* England */      "GR" /* Greece */    "HR" /* Croatia */
  "HU" /* Hungary */      "IL" /* Israel */    "IN" /* India */
  "IT" /* Italy */        "JP" /* Japan */     "KP" /* North Korea */
  "KR" /* South Korea */  "NE" /* Niger */     "NL" /* Netherlands */
  "PO" /* Poland */       "PT" /* Portugal */  "RU" /* Russia */
  "SE" /* Sweden */       "SK" /* Slovakia */  "TR" /* Turkey */
  "UA" /* Ukraine */      "US" /* USA */;

// Converts two country characters to a single number.
#define COUNTRY_NUM(c1,c2) ((((int) c2) << 8) | ((int) c1))

typedef struct
{
  // purely aesthetic:
  char name[NAME_MAX_LEN + 1];
  uint8_t look;         // bits: 0: race, 1: hair, 3: shoes, 2: foot (L/R)

  // personality, may affect play:
  uint8_t ageYears;     // will affect some attributes
  uint8_t heightCm;     // will affect some attributes
  uint8_t weightKg;     // will affect some attributes
  uint8_t ego;          // less likely to pass etc.
  uint8_t aggressivity; // more likely to foul, go for ball etc.
  
  // skills (higher = better):
  uint8_t speed;        // faster run, turns, decisions
  uint8_t strength;     // faster and longer kicks/throws, better fights
  uint8_t stamina;      // less likely to trip or rest
  uint8_t accuracy;     // more precise shots and throws, better catch
  uint8_t agility;      // better 1v1, catching, less likely to trip
  uint8_t iq;           // faster and better decisions, fewer blunders

  // dynamic attributes:
  uint8_t stateDir;        // state and direction combined
  uint8_t nextStateChange; // frame till next state change
  int16_t position[2];     // pixel position on the pitch

  uint8_t goalsFouls;      // goals scored (lower 4 bits), fouls committed
} Player;

//====== global variables ======

#define CAMERA_FOLLOW_NONE 255
#define CAMERA_FOLLOW_BALL 128

uint32_t currentRand;                   // for LCG
uint8_t playerSortArray[PLAYERS_TOTAL]; // helper for drawing
uint8_t tmpImage[2 + PLAYER_IMAGE_W * PLAYER_IMAGE_H]; // helper image
int cameraPos[2];                       // screen top left pos. in pixels
uint8_t cameraFollowMode;               /* follow mode of camera:
                                           0 to PLAYERS_TOTAL -1: follow player
                                           128: follow ball
                                           255: no follow */
#define MENU_ITEMS 9
#define MENU_STATE_OFF 255

uint8_t menuState;
uint32_t menuGameNumber;

#define GAME_STATE_PLAYING    0         // normal play
#define GAME_STATE_AFTER_GOAL 1         // goal was scored, also start of game
#define GAME_STATE_FREE_KICK  2         // free kick, pen., throw, corner etc.
#define GAME_STATE_KICKOFF    3         // goal keeper to kick off
#define GAME_STATE_HALFTIME   4
#define GAME_STATE_END        5

#define DISPLAY_TEXT_SIZE     10

struct
{
  uint16_t number;      /* complete game seed, structure (from lowest bits):
                           5 bits: country 1
                           5 bits: country 2
                           8 bits: season
                           14 bits: random seed */
  uint8_t state;

  uint8_t score[2];
  uint16_t stateFrame;  // frames elapsed since start of state

  char displayText[DISPLAY_TEXT_SIZE];
  uint8_t displayTextCountdown;

  uint32_t playFrame;   // frames of play time since start of match
  uint16_t nextTripIn;  // frames until next random trip

  uint8_t playerToKick; // player index, for free kicks, throws etc.

  Player players[PLAYERS_TOTAL]; /* first team, then second, then referee,
                                    player 0 on a team is goal keeper */
  struct
  {
    int16_t position[2];   // current, interpolated position
    int16_t posFrom[2];    // start position
    int16_t vecTo[2];      // vector to end position
    uint8_t height;        // current height
    uint8_t maxHeight;     // maximum height to reach
    uint8_t interpolParam; // determines position between start and end pos.
    int8_t  lastPlayer;    // player that touched the ball last
    int8_t  player;        // player in possesion or -1
  } ball;
} game;

//====== functions ======

void clampAttribute(uint8_t *attr)
{
  if (*attr > ATTRIBUTE_MAX)
    *attr = ATTRIBUTE_MAX;
}

int clamp(int v, int min, int max)
{
  return v > max ? max : (v < min ? min : v);
}

int16_t distTaxi(int16_t x0, int16_t y0, int16_t x1, int16_t y1)
{
  return (x0 > x1 ? x0 - x1 : x1 - x0) + (y0 > y1 ? y0 - y1 : y1 - y0);  
}

void gameSetState(uint8_t state)
{
  DEBUG_PRINT("changing game state");
  game.state = state;
  game.stateFrame = 0;
}

// turns one step towards desired direction
uint8_t dirTurnTowards(uint8_t current, uint8_t desired)
{
  if (desired == current)
    return current;

  uint8_t opposite = (current + 4) % 8;

  return ((current + 8) + // god bless this mess
    ((((desired > opposite && desired < current) |
      (desired > current && desired < opposite)) ==
      (opposite < current)) ? -1 : 1)) % 8;
}

void dirToVector(uint8_t dir, int16_t v[2])
{
  v[0] = (dir == PLAYER_DIR_R || dir == PLAYER_DIR_UR ||
     dir == PLAYER_DIR_DR) +
    ((dir == PLAYER_DIR_L || dir == PLAYER_DIR_UL ||
      dir == PLAYER_DIR_DL) ? -1 : 0);

  v[1] = (dir == PLAYER_DIR_D || dir == PLAYER_DIR_DR ||
     dir == PLAYER_DIR_DL) +
    ((dir == PLAYER_DIR_U || dir == PLAYER_DIR_UR ||
      dir == PLAYER_DIR_UL) ? -1 : 0);
}

char hexDigitToChar(uint8_t digit)
{
  return digit < 10 ? ('0' + digit) : ('A' + digit - 10);
}

uint32_t createGameNum(uint8_t country1, uint8_t country2, uint8_t season,
  uint16_t seed)
{
  seed &= 0x3fff;

  return ((uint32_t) country1) | (((uint32_t) country2) << 5) |
    (((uint32_t) season) << 10) | (((uint32_t) seed) << 18);
}

uint16_t gameNumToSeed(uint32_t n)
{
  return n >> 18;
}

uint8_t gameNumToCountryNum(uint32_t n, uint8_t countryNum)
{
  return (n >> (countryNum == 0 ? 0 : 5)) & 0x1f;
}

const char *gameNumToCountryCode(uint32_t n, uint8_t countryNum)
{
  return countryNames + 2 * gameNumToCountryNum(n,countryNum);
}

uint8_t gameNumToSeason(uint32_t n)
{
  return (n >> 10) & 0xff;
}

// 16 bit random num, 32 bit period
unsigned int rnd(void)
{
  currentRand = 22695477 * currentRand + 123;
  return currentRand >> 16;
}

// normal distribution random number
unsigned int rndNorm(unsigned int maximum)
{
  // enforce order of evaluation:
  unsigned int result = rnd() % maximum;
  result += rnd() % maximum;
  result += maximum - 1 - rnd() % maximum;
  result += maximum - 1 - rnd() % maximum;
  return result / 4;
}

uint8_t getPlayerSkill(const Player *p)
{
  return (((int) p->speed) + ((int) p->strength) + ((int) p->stamina) +
    ((int) p->accuracy) + ((int) p->agility) + ((int) p->iq)) / 6;
}

int areaHasAtLeastPlayers(int16_t centerX, int16_t centerY, int16_t dist,
  uint8_t team, uint8_t count)
{
  centerX -= dist;
  centerY -= dist;
  dist *= 2;

  for (int i = team * PLAYERS_IN_TEAM;
    i < PLAYERS_IN_TEAM + team * PLAYERS_IN_TEAM; ++i)
    if (game.players[i].position[0] >= centerX &&
      game.players[i].position[0] <= centerX + dist &&
      game.players[i].position[1] >= centerY &&
      game.players[i].position[1] <= centerY + dist)
    {
      count--;
 
      if (!count)
        return 1;
    }

  return 0;
}

void ballReset(int16_t x, int16_t y)
{
  game.ball.posFrom[0] = x;
  game.ball.posFrom[1] = y;
  game.ball.position[0] = x;
  game.ball.position[1] = y;
  game.ball.vecTo[0] = 0;
  game.ball.vecTo[1] = 0;
  game.ball.height = 0;
  game.ball.player = -1;
  game.ball.lastPlayer = -1;
  game.ball.interpolParam = BALL_ANIM_FRAMES + 1;
}

void playerGetStartPos(uint8_t index, int16_t result[2])
{
  if (index == PLAYERS_IN_TEAM * 2) // referee?
  {
    result[0] = PITCH_W / 2;
    result[1] = PITCH_H / 2 - SQUARE_SIZE;
    return;
  }

  int flip = 0;

  if (index >= PLAYERS_IN_TEAM)
  {
    flip = 1;
    index -= PLAYERS_IN_TEAM;
  }

  switch (index)
  {
    case 0:
      result[0] = SQUARE_SIZE * 2;
      result[1] = PITCH_H / 2;
      break;

    case 1: // fall through
    case 2:
      result[0] = PITCH_W / 6;
      result[1] = PITCH_H / 3;
      break;

    case 3: // fall through
    case 4:
      result[0] = PITCH_W / 4 + SQUARE_SIZE;
      result[1] = PITCH_H / 4;
      break;

    case 5: // fall through
    case 6:
      result[0] = PITCH_W / 2 - SQUARE_SIZE - SQUARE_SIZE / 2;
      result[1] = 3 * SQUARE_SIZE;
      break;

    default:
      result[0] = PITCH_W / 2 - 2 * SQUARE_SIZE;
      result[1] = PITCH_H / 2;
      break;
  }

  if (index == 2 || index == 4 || index == 6)
    result[1] = PITCH_H - result[1];

  if (flip)
    result[0] = PITCH_W - result[0];
}

// Shoots ball from its current pos to new pos, reaching given max height.
void ballShoot(int16_t toX, int16_t toY, uint8_t maxHeight, uint8_t accuracy)
{
  DEBUG_PRINT("ball shoot");

  int spread = 32 - (32 * accuracy) / 128;

  spread += spread == 0;

  toX = toX - spread / 2 + (rnd() % spread);
  toY = toY - spread / 2 + (rnd() % spread);

  game.ball.player = -1;

  game.ball.vecTo[0] = toX - game.ball.position[0];
  game.ball.vecTo[1] = toY - game.ball.position[1];

  if (game.ball.position[0] == toX && game.ball.position[1] == toY)
    game.ball.interpolParam = BALL_ANIM_FRAMES + 1;
  else
  {
    game.ball.posFrom[0] = game.ball.position[0];
    game.ball.posFrom[1] = game.ball.position[1];
    game.ball.interpolParam = 0;
    game.ball.maxHeight = maxHeight;
  }
}

void gameDisplayText(const char *text)
{
#if DEBUG
  if (*text != 0)
    printf("[%4x: %s]\n",game.playFrame,text);
#endif

  int i = 0;

  game.displayTextCountdown = SAF_FPS * 3;

  while (text && i < DISPLAY_TEXT_SIZE - 1)
  {
    game.displayText[i] = *text;
    text++;
    i++;
  }

  game.displayText[i] = 0;
}

Player playerGenerate(const char *country2Chars, uint8_t index, uint8_t season)
{
  uint32_t seedBackup = currentRand;

  currentRand = ((uint32_t) index) |
    (((uint32_t) country2Chars[0]) << 8) |
    (((uint32_t) country2Chars[1]) << 16) |
    (((uint32_t) season) << 24);

  Player p;

#define IS_COUNTRY(c1,c2) (country2Chars[0] == c1 && country2Chars[1] == c2)

  // base values:
  p.weightKg = 72;
  p.heightCm = 175;
  p.iq = 100;
  p.ego = 90;
  p.ageYears = 27;
  p.aggressivity = 90;
  p.speed = 95;
  p.strength = 78;
  p.stamina = 95;
  p.accuracy = 90;
  p.agility = 90;

  char nameLetters[] =              // letters to make names from
    "rtpsdfghjklzcbnm"              // first letters
    "aeiouyeo";                     // second letters

  uint8_t blackProbability = 10;    // probability of being black, max 127
  uint8_t blondeProbability = 30;
  uint8_t countrySkill = 50;

  switch (COUNTRY_NUM(country2Chars[0],country2Chars[1]))
  {
    case COUNTRY_NUM('U','S'):
      p.weightKg += 10;
      blackProbability *= 5;
      p.iq -= 6;
      p.strength += 7;
      countrySkill -= 8;
      p.ego += 32;
      p.aggressivity += 20;
      break;

    case COUNTRY_NUM('I','N'):
      blackProbability *= 4;
      blondeProbability /= 4;
      break;

    case COUNTRY_NUM('K','P'):
      p.weightKg -= 4;
      p.heightCm -= 3;
      // fall through
    case COUNTRY_NUM('J','P'):
      // fall through
    case COUNTRY_NUM('K','R'):
      // fall through
    case COUNTRY_NUM('C','N'):
      p.iq += 8;
      p.weightKg -= 5;
      p.heightCm -= 5;
      blackProbability = 0;
      blondeProbability = 5;
      p.agility += 5;
      p.ego -= 25;
      p.aggressivity -= 11;
      countrySkill -= 35;
      nameLetters[0] = 'i';
      nameLetters[2] = 'o';
      nameLetters[21] = '-';
      nameLetters[22] = '\'';
      nameLetters[23] = 'g';
      nameLetters[24] = 'c';
      break;

    case COUNTRY_NUM('I','L'):
      p.iq += 10;
      blackProbability = 0;
      break;

    case COUNTRY_NUM('N','E'):
      p.iq -= 15;
      p.weightKg -= 5;
      blackProbability = 120;
      blondeProbability = 10;
      break;

    case COUNTRY_NUM('F','R'):
      blackProbability *= 4;
      nameLetters[22] = '\'';
      countrySkill += 18;
      p.ego += 5;
      break;

    case COUNTRY_NUM('S','E'):
      blondeProbability = 55;
      break;

    case COUNTRY_NUM('B','R'):
      countrySkill += 10;
      // fall through
    case COUNTRY_NUM('P','T'):
      countrySkill += 10;
      // fall through
    case COUNTRY_NUM('A','R'):
      p.agility += 20;
      blondeProbability /= 2;
      countrySkill += 15;
      break;

    case COUNTRY_NUM('E','S'):
      countrySkill += 20;
      blondeProbability /= 2;
      p.agility += 4;
      break;

    case COUNTRY_NUM('G','B'):
      countrySkill += 20;
      break;

    case COUNTRY_NUM('I','T'):
      countrySkill += 14;
      break;

    case COUNTRY_NUM('R','U'):
      p.aggressivity += 12;
      p.strength += 7;
      blackProbability = 2;
      break;

    case COUNTRY_NUM('A','Q'):
      countrySkill -= 20;
      break;

    default: break;
  }

  // randomize
  p.weightKg += -36 + rndNorm(72);
  p.heightCm += -30 + rndNorm(60);
  p.ageYears += -17 + rndNorm(34);
  p.iq += -50 + rndNorm(100);

  p.ego += -20 + rndNorm(40);
  p.aggressivity += -22 + rndNorm(44);

  p.speed -= p.ageYears / 8;
  p.stamina -= p.ageYears / 7;
  p.strength -= p.ageYears / 6;
  p.iq -= p.ageYears / 8;

  p.speed -= p.weightKg / 8;
  p.strength += p.weightKg / 16;
  p.strength += p.heightCm / 16;

  int skillPoints = rndNorm(countrySkill);

  for (int i = 0; i < 4; ++i) // take away some skill point for reshuffling
  {
    #define TAKE_AWAY 15

    switch (rnd() % 5)
    {
      case 0: p.speed -= TAKE_AWAY; break;
      case 1: p.stamina -= TAKE_AWAY; break;
      case 2: p.strength -= TAKE_AWAY; break;
      case 3: p.accuracy -= TAKE_AWAY; break;
      case 4: p.agility -= TAKE_AWAY; break;
      default: break;
    }

    skillPoints += TAKE_AWAY;

    #undef TAKE_AWAY
  }

  while (skillPoints) // now pour the skill points back in
  {
    switch (rnd() % 5)
    {
      case 0: p.speed++; break;
      case 1: p.stamina++; break;
      case 2: p.strength++; break;
      case 3: p.accuracy++; break;
      case 4: p.agility++; break;
      default: break;
    }

    skillPoints--;
  }

  for (int i = 0; i < NAME_MAX_LEN + 1; ++i) // make the name
  {
    p.name[i] = 0;

    if ((i == NAME_MAX_LEN) | ((i > 2) & (rnd() % 16 < 2)))
    {
      if (!(
        (p.name[i - 1] >= 'A' && p.name[i - 1] <= 'Z') ||
        (p.name[i - 1] >= 'a' && p.name[i - 1] <= 'z')))
        p.name[i] = 'u'; // let last name be always a letter

      break;
    }

    int r = rnd();

    p.name[i] = (rnd() % 64 < 58) ?
      ((unsigned int) nameLetters[((i % 2) * 16) + (r % (i % 2 ? 8 : 16))])
      : (unsigned int) ('a' + r % 26); // sometimes add completely random letter

    if (i == 0)
      p.name[i] = (p.name[i] - 'a') + 'A';
  }

  p.look = 0;

  if ((rnd() % 128) < blackProbability)
    p.look |= RACE_BIT;

  if ((rnd() % 128) < blondeProbability)
    p.look |= HAIR_BIT;

  if ((rnd() % 128) < 36)
    p.look |= FOOT_BIT;

  if (rnd() & 0x80)
    p.look |= SHOE_BIT;

  if (p.look & RACE_BIT)
  {
    // less intelligent but faster
    p.iq -= 8;
    p.speed += 8;
  }

  clampAttribute(&p.ego);
  clampAttribute(&p.aggressivity);
  clampAttribute(&p.speed);
  clampAttribute(&p.strength);
  clampAttribute(&p.stamina);
  clampAttribute(&p.accuracy);
  clampAttribute(&p.agility);

  currentRand = seedBackup;

  p.position[0] = PITCH_W / 2;
  p.position[1] = PITCH_H / 2;
  p.stateDir = PLAYER_STATE_STANDING | PLAYER_DIR_D;
  p.nextStateChange = 0;
  p.goalsFouls = 0;

  return p;
}

// finds a player to kick a free kick (also throw in etc.)
uint8_t findPlayerToKick(uint8_t team)
{
  uint8_t start = ((team != 0) * PLAYERS_IN_TEAM) + 1; // exclude goal keeper
  int16_t bestD = 10000;
  uint8_t best = start;

  for (int i = start; i < start + PLAYERS_IN_TEAM - 1; ++i)
  {
    int16_t d = distTaxi(game.players[i].position[0],
      game.players[i].position[1],game.ball.position[0],game.ball.position[1]);

    if (d < bestD)
    {
      best = i;
      bestD = d;
    }
  }

  return best;
}

void playerMisconducts(uint8_t index)
{
  Player *p = game.players + index;

  DEBUG_PRINT("misconduct");
  game.playerToKick = findPlayerToKick(1 - (index / PLAYERS_IN_TEAM));

  if (
    p->position[1] > (PITCH_H / 2 - PENALTY_AREA_H / 2) &&
    p->position[1] < (PITCH_H / 2 + PENALTY_AREA_H / 2) &&
    (
      (((index / PLAYERS_IN_TEAM) == 1) && p->position[0]
        > PITCH_W - SQUARE_SIZE - PENALTY_AREA_W) ||
      (((index / PLAYERS_IN_TEAM) == 0) && p->position[0]
        < SQUARE_SIZE + PENALTY_AREA_W)
    ))
  {
    gameDisplayText("penalty!");
    DEBUG_PRINT("penalty!");
    game.ball.vecTo[0] = SQUARE_SIZE + PENALTY_AREA_W;

    if (game.ball.position[0] > PITCH_W / 2)
      game.ball.vecTo[0] = PITCH_W - game.ball.vecTo[0];

    game.ball.vecTo[0] = game.ball.vecTo[0] - game.ball.position[0];

    game.ball.vecTo[1] = PITCH_H / 2 - game.ball.position[1];
  }
  else
    ballReset(game.ball.position[0],game.ball.position[1]);

  SAF_playSound(SOUND_WHISTLE);

  gameSetState(GAME_STATE_FREE_KICK);
}

// Converts two points to direction.
uint8_t pointsToDir(int16_t p0[2], int16_t p1[2])
{
  int16_t tangentTimes2 = (2 * (p0[0] - p1[0])) /
    (p0[1] != p1[1] ? (p0[1] - p1[1]) : 1); 

  switch ((p1[0] >= p0[0]) | ((p1[1] >= p0[1]) << 1) |
    ((tangentTimes2 != 0) << 4) |
    (((tangentTimes2 > 4) | (tangentTimes2 < -4)) << 5))
  {
    case 0x01: // fall through
    case 0x00: return PLAYER_DIR_U;  break;
    case 0x10: return PLAYER_DIR_UL; break;
    case 0x20: return PLAYER_DIR_UL; break;
    case 0x11: return PLAYER_DIR_UR; break;
    case 0x32: // fall through
    case 0x30: return PLAYER_DIR_L;  break;
    case 0x33: // fall through
    case 0x31: return PLAYER_DIR_R;  break;
    case 0x03: // fall through
    case 0x02: return PLAYER_DIR_D;  break;
    case 0x12: return PLAYER_DIR_DL; break;
    case 0x13: return PLAYER_DIR_DR; break;
    default:   return PLAYER_DIR_U;
  }
}

void playerShoot(uint8_t index, int16_t point[2], uint8_t height)
{
  Player *p = game.players + index;
  int16_t dest[2];

  dest[0] = point[0];
  dest[1] = point[1];

  game.ball.player = -1;

  p->stateDir = // we just rotate immediately here 
    pointsToDir(p->position,dest) |
    ((p->look & FOOT_BIT) ? PLAYER_STATE_KICK_L : PLAYER_STATE_KICK_R);

  p->nextStateChange = clamp(
    PLAYER_BASE_STATE_FRAMES - ((p->agility + p->speed) / 32),
    0,PLAYER_BASE_STATE_FRAMES * 10);

  if (distTaxi(dest[0],dest[1],game.ball.position[0],game.ball.position[1])
    > DIST_CLOSE + p->strength / 16)
    for (int i = 0; i < 2; ++i)
    {
      dest[i] -= p->position[i];
      dest[i] = (dest[i] * clamp(p->strength + 10,0,128)) / 128;
      dest[i] += p->position[i];
    }

  ballShoot(dest[0],dest[1],height,p->accuracy);

  SAF_playSound(SOUND_BUMP);
}

void playerPass(uint8_t index)
{
  DEBUG_PRINT("pass");

  int bestScore = -1;
  Player *p = game.players +
    (index / PLAYERS_IN_TEAM) * PLAYERS_IN_TEAM;
  Player *bestP = p + 1;

  int16_t enemyGoalX = index < PLAYERS_IN_TEAM ? PITCH_W : 0;

  for (int i = 0; i < PLAYERS_IN_TEAM; ++i)
  {
    if (p != (game.players + index))
    {
      int score = 1000 -
        (distTaxi(enemyGoalX,PITCH_H / 2,p->position[0],p->position[1]) +
         distTaxi(p->position[0],p->position[1],
           game.players[i].position[0],game.players[i].position[1]))
           / SQUARE_SIZE;

      if ((score > bestScore) & ((rnd() % 128) < p->iq))
      {
        bestScore = score;
        bestP = p;
      }
    }
      
    p++;
  }

  playerShoot(index,bestP->position,0);
}

void playerTrip(uint8_t index)
{
  game.players[index].stateDir = PLAYER_STATE_LYING |
    (game.players[index].stateDir & PLAYER_DIR_MASK);

  game.players[index].nextStateChange =
    clamp(2 * PLAYER_BASE_STATE_FRAMES - game.players[index].speed / 2,
    0,PLAYER_BASE_STATE_FRAMES * 3) +
    (game.players[index].ego / 4); // bigger ego => longer drama

  game.players[index].nextStateChange += PLAYER_BASE_STATE_FRAMES / 3;

  if (game.ball.player == index)
  {
    int16_t dest[2];

    dirToVector(game.players[index].stateDir & PLAYER_DIR_MASK,dest);

    dest[0] = game.players[index].position[0] + 2 * SQUARE_SIZE;
    dest[1] = game.players[index].position[1] + 2 * SQUARE_SIZE;

    game.ball.player = -1;

    ballShoot(dest[0],dest[1],0,100);
  }

  SAF_playSound(SOUND_BOOM);
}

// The player will start turning or running towards given point.
void playerWantsToRunTo(uint8_t index, int16_t point[2])
{
  point[0] = clamp(point[0],0,PITCH_W);
  point[1] = clamp(point[1],0,PITCH_H);

  Player *p = game.players + index;

  if (p->position[0] == point[0] && p->position[1] == point[1])
  {
    p->stateDir =
     (p->stateDir & PLAYER_DIR_MASK) | PLAYER_STATE_STANDING;
  }
  else
  {
    uint8_t direction = p->stateDir & PLAYER_DIR_MASK;
    uint8_t desired = pointsToDir(p->position,point);
    uint8_t turnSpeed = clamp((p->speed + p->agility) / 64,1,10);

    while (turnSpeed && direction != desired)
    {
      direction = dirTurnTowards(direction,desired);
      turnSpeed--;
    }

    p->stateDir =
      (direction == desired ? PLAYER_STATE_RUNNING : PLAYER_STATE_STANDING)
       | direction;
  }

  p->nextStateChange = PLAYER_BASE_STATE_FRAMES / 3;
}

// Bounces a ball horizontally.
void ballHorizontalBounce(int right)
{
  right = right ? PITCH_W - SQUARE_SIZE : SQUARE_SIZE;
  game.ball.posFrom[0] = -1 * game.ball.posFrom[0] + 2 * right;
  game.ball.position[0] = -1 * game.ball.position[0] + 2 * right;
  game.ball.vecTo[0] *= -1;
}

void ballUpdate(void)
{
  if (game.ball.player != -1) // someone holds the ball?
  {
    game.ball.interpolParam = BALL_ANIM_FRAMES + 1;

    dirToVector(game.players[game.ball.player].stateDir & PLAYER_DIR_MASK,
      game.ball.position);

    game.ball.position[0] = 2 * game.ball.position[0] +
      game.players[game.ball.player].position[0];
    game.ball.position[1] = 2 * game.ball.position[1] +
      game.players[game.ball.player].position[1];
  }
  else
  {
    // no one holds the ball

    if (game.ball.interpolParam <= BALL_ANIM_FRAMES)
    {
      game.ball.position[0] = game.ball.posFrom[0] +
        (game.ball.interpolParam * game.ball.vecTo[0]) / BALL_ANIM_FRAMES;

      game.ball.position[1] = game.ball.posFrom[1] +
        (game.ball.interpolParam * game.ball.vecTo[1]) / BALL_ANIM_FRAMES;

      int newHeight = game.ball.interpolParam;
      newHeight -= BALL_ANIM_FRAMES / 2;
      newHeight *= newHeight;
      newHeight = ((BALL_ANIM_FRAMES / 2) * (BALL_ANIM_FRAMES / 2)) - newHeight;
      newHeight = (game.ball.maxHeight * newHeight) /
        ((BALL_ANIM_FRAMES / 2) * (BALL_ANIM_FRAMES / 2));

      if (newHeight == 0 && game.ball.height != 0)
        SAF_playSound(SOUND_BUMP);

      game.ball.height = newHeight;
      game.ball.interpolParam++;

      if (game.ball.interpolParam >= BALL_ANIM_FRAMES) // bounce
        ballShoot(game.ball.position[0] + game.ball.vecTo[0] / BALL_BOUNCE,
        game.ball.position[1] + game.ball.vecTo[1] / BALL_BOUNCE,
        game.ball.maxHeight / BALL_BOUNCE,128);
    }

    if (game.ball.height <= BALL_CATCH_HEIGHT) // see if someone catches it
    {
      // make it more fair, don't prioritize one team in ball pick:
      int index = (game.playFrame % 2) * PLAYERS_IN_TEAM;

      for (int i = 0; i < PLAYERS_IN_TEAM * 2; ++i)
      {
        uint8_t state = game.players[index].stateDir & PLAYER_STATE_MASK;
        uint8_t goalieBonus = ((index % PLAYERS_IN_TEAM) == 0) ? 2 : 0;

        if (
          state != PLAYER_STATE_KICK_R &&
          state != PLAYER_STATE_KICK_L &&
          state != PLAYER_STATE_LYING &&
          distTaxi(
            game.players[index].position[0],
            game.players[index].position[1],
            game.ball.position[0],
            game.ball.position[1]) <= (DIST_AT + goalieBonus) &&
          ( // prob. of catch depends on ball trajectory len. and agility 
            (rnd() % 128 +
            distTaxi(0,0,game.ball.vecTo[0],game.ball.vecTo[1]) /
            (2 * SQUARE_SIZE)) <= ((unsigned int)
            (game.players[index].agility + game.players[index].accuracy) / 2
            + goalieBonus * 4)
          )
          )
        {
          game.ball.player = index;
          game.ball.lastPlayer = index;
          game.ball.height = 0;

          if (game.state == GAME_STATE_PLAYING)
            gameDisplayText(game.players[index].name);

          if ((game.ball.height >= BALL_HAND_HEIGHT) & ((rnd() % 128) < 20))
          {
            gameDisplayText("hand!");
            DEBUG_PRINT("hand!");
            playerMisconducts(index);
          }

          break;
        }

        index = (index + 1) % (PLAYERS_IN_TEAM * 2);
      }
    }
  }
}

void playerFightForBall(uint8_t index)
{
  DEBUG_PRINT("fight for ball");

  if (game.ball.player < 0)
    return;

  if ((rnd() % 128) <
    ((3 * ((int) game.players[index].aggressivity) / 4)))
  {
    // foul
    DEBUG_PRINT("player attacked");

    game.players[index].stateDir =
      (game.players[index].stateDir & PLAYER_DIR_MASK) |
      ((game.players[index].look & FOOT_BIT) ?
        PLAYER_STATE_KICK_L : PLAYER_STATE_KICK_R);

    playerTrip(game.ball.player);

    if ((rnd() % 128) <
      ((unsigned int) 40 + game.players[index].aggressivity / 16))
    {
      gameDisplayText("foul!");
      DEBUG_PRINT("foul!");
      playerMisconducts(index);

      if ((game.players[index].goalsFouls & 0xf0) != 0xf0)
        game.players[index].goalsFouls += 0x10;
    }
  }
  else
  {
    // normal fight

    // enforce evaluation order:
    unsigned int cond =
      (rnd() % (game.players[index].agility + 1 +
      game.players[index].aggressivity % 32));

    cond = cond >
     (rnd() % (game.players[game.ball.player].agility + 12));

    if (cond)
    {
      DEBUG_PRINT("won the ball");
      game.ball.player = index;
    }
  }
}

void playerShootOnGoal(uint8_t index)
{
  gameDisplayText("shoots!");
  DEBUG_PRINT("shoot on goal!");

  int16_t dest[2], stren;

  stren = game.players[index].strength;

  dest[0] = rnd() % (2 + stren / 8);
  dest[0] = (index / PLAYERS_IN_TEAM) ?
    (SQUARE_SIZE - dest[0]) : (PITCH_W - SQUARE_SIZE + dest[0]);

  dest[1] = PITCH_H / 2 - GOAL_WIDTH / 2 + (rnd() % GOAL_WIDTH);

  playerShoot(index,dest,rnd() % (2 + stren / 16));
}

void playerUpdate(uint8_t index)
{
  Player *p = game.players + index;

  uint8_t state = p->stateDir & PLAYER_STATE_MASK;
  uint8_t dir = p->stateDir & PLAYER_DIR_MASK;
  int16_t dest[2]; // helper
  uint8_t myTeam = index / PLAYERS_IN_TEAM;

  dest[0] = PITCH_W / 2;
  dest[1] = PITCH_H / 2;

  if (p->nextStateChange == 0)
  {
    // by default just stand:
    p->stateDir = (p->stateDir & PLAYER_DIR_MASK) | PLAYER_STATE_STANDING;
    p->nextStateChange = PLAYER_BASE_STATE_FRAMES;

    if (index == 2 * PLAYERS_IN_TEAM) // am I the referee?
    {
      // just stay near the ball

      for (int i = 0; i < 2; ++i)
        dest[i] = clamp(p->position[i],game.ball.position[i] - SQUARE_SIZE * 3,
          game.ball.position[i] + SQUARE_SIZE * 3);

      playerWantsToRunTo(index,dest);
    }
    else if (index % PLAYERS_IN_TEAM == 0) // am I goal keeper?
    {
      if (game.ball.player == index) // I have the ball?
      {
        if (game.ball.height == 0)
        {
          // pick it up
          DEBUG_PRINT("picking up the ball");
          game.ball.height = 5;
          p->nextStateChange = 2 * PLAYER_BASE_STATE_FRAMES + rnd() % 32;
        }
        else
        {
          DEBUG_PRINT("kicking the ball out");
          dest[0] = PITCH_W / 4 + rnd() % (PITCH_W / 4) + SQUARE_SIZE;

          if (index >= PLAYERS_IN_TEAM)
            dest[0] = PITCH_W - dest[0];

          dest[1] = PITCH_H / 2 - SQUARE_SIZE * 4 + rnd() % (SQUARE_SIZE * 8);

          playerShoot(index,dest,p->strength / 8 + rnd() % 8);
        }
      }
      else
      {
        int16_t dist = distTaxi(p->position[0],p->position[1],
          game.ball.position[0],game.ball.position[1]);

        if (dist <= DIST_AT)
          playerFightForBall(index);
        else if ((dist <= (DIST_CLOSE / 2)) & ((rnd() % 128) < 85)) // near?
        {
          dest[0] = game.ball.position[0];
          dest[1] = game.ball.position[1];
          playerWantsToRunTo(index,dest);
          p->nextStateChange /= 4;
        }
        else if (dist < 2 * DIST_CLOSE) // a bit close
        {
          dest[0] = index == 0 ? 2 * SQUARE_SIZE : (PITCH_W - 2 * SQUARE_SIZE);
          dest[1] = clamp(game.ball.position[1],PITCH_H / 2 - GOAL_WIDTH / 2,
            PITCH_H / 2 + GOAL_WIDTH / 2);

          playerWantsToRunTo(index,dest);

          dir = p->stateDir & PLAYER_DIR_MASK;

          if ((  
            (myTeam == (game.playFrame < PLAY_FRAMES / 2))
              && dir == PLAYER_DIR_R) ||
            (myTeam == (game.playFrame >= PLAY_FRAMES / 2)
              && dir == PLAYER_DIR_L))
          {
            /* here rotate immediatelly (fixes a visual bug when goalie stares
            into own goal */
            p->stateDir = (p->stateDir & PLAYER_STATE_MASK) |
              pointsToDir(p->position,dest);
          }

          p->nextStateChange /= 2;
        }
        else
        {
          p->stateDir = PLAYER_STATE_STANDING |
            (index == 0 ? PLAYER_DIR_R : PLAYER_DIR_L);
        }
      }
    }
    else if (game.ball.player == -1) // no one has the ball?
    {
      // if close, run for it, else decide randomly if run or not
      if ((distTaxi(p->position[0],p->position[1],game.ball.position[0],
        game.ball.position[1]) < DIST_CLOSE) | ((rnd() % 127) < 30))
        playerWantsToRunTo(index,game.ball.position);
      else if (rnd() % 128 < 64)
      {
        // run to start position
        playerGetStartPos(index,dest);
        playerWantsToRunTo(index,dest);
      }

      // else just stand
    }
    else if (game.ball.player == index) // I have the ball?
    {
      // enforce evaluation order:
      int doBlunder = (rnd() % 16) == 0;
      doBlunder &= (rnd() % 128) > p->iq;

      if (doBlunder)
      {
        gameDisplayText("oh no!");
        DEBUG_PRINT("blunder");

        for (int i = 0; i < 2; ++i)
          dest[i] = p->position[i] - 8 * SQUARE_SIZE +
            (rnd() % (16 * SQUARE_SIZE));

        playerShoot(index,dest,rnd() % 16);
      }
      else
      {
        // own goal coords:
        dest[0] = index >= PLAYERS_IN_TEAM ? PITCH_W : 0;
        dest[1] = PITCH_H / 2;

        if (distTaxi(p->position[0],p->position[1],dest[0],dest[1]) < DIST_CLOSE)
        {
          // close to own goal

          if (areaHasAtLeastPlayers(p->position[0],p->position[1],DIST_CLOSE,
            !myTeam,1))
          {
            // enemies nearby => kick the ball away

            dest[0] = (((int16_t) p->strength) *
              ((p->position[0] >= PITCH_W / 2) ?
              -1 * PITCH_H / 2 : PITCH_H / 2)) / 128;

            dest[1] = -1 * PITCH_H / 4 + (rnd() % (PITCH_H / 2));

            dest[0] += p->position[0];
            dest[1] += p->position[1];

            playerShoot(index,dest,p->strength / 8);
          }
          else
          {
            dest[0] = PITCH_W / 2;
            dest[1] = PITCH_H / 2 - 4 * SQUARE_SIZE + rnd() % (SQUARE_SIZE * 8);
            playerWantsToRunTo(index,dest);
          }
        }
        else
        {
          // not close to own goal

          dest[0] = index >= PLAYERS_IN_TEAM ? 0 : PITCH_W; // enemy goal coord

          int enemyGoalDist = distTaxi(p->position[0],p->position[1],dest[0],
            dest[1]);

          // enforce evaluation order
          int cond = ((enemyGoalDist < DIST_CLOSE) & ((rnd() % 2)));
          cond |= (enemyGoalDist < 3 * DIST_CLOSE / 2) &
            ((rnd() % 16) <= (p->ego / 32));

          if (cond)
          {
            // close to enemy goal + some chance
            playerShootOnGoal(index);
          }
          else
          {
            // somewhere else

            // where I'd like to run:
            dest[0] = p->position[0] + (index < PLAYERS_IN_TEAM ?
              4 * SQUARE_SIZE : -4 * SQUARE_SIZE);

            dest[1] = clamp(
              ((p->position[1] + PITCH_H / 2) / 2) - (SQUARE_SIZE * 4)
              + rnd() % (SQUARE_SIZE * 8),
              SQUARE_SIZE + SQUARE_SIZE / 2,
              PITCH_H - SQUARE_SIZE - SQUARE_SIZE / 2);

            if (areaHasAtLeastPlayers(dest[0],dest[1],SQUARE_SIZE,!myTeam,1))
            {
              // occupied by enemies

              if ((rnd() > p->ego) & !(
                  // in front of goal only pass if there is any teammate
                  (enemyGoalDist < 3 * DIST_CLOSE / 4) &&
                  !areaHasAtLeastPlayers(
                   game.players[myTeam * PLAYERS_IN_TEAM].position[0],
                   game.players[myTeam * PLAYERS_IN_TEAM].position[1],
                   DIST_CLOSE / 2,myTeam,2)))
                playerPass(index);
              else
              {
                DEBUG_PRINT("going sideways");

                dest[0] = p->position[0] + rnd() % SQUARE_SIZE;
                dest[1] = PITCH_H / 2 +
                  ((rnd() & 0x01) ? PITCH_W / 3 : PITCH_W / -3);

                playerWantsToRunTo(index,dest);
              }
            }
            else
            {
              DEBUG_PRINT("going forward");
              playerWantsToRunTo(index,dest);
            }
          }
        }
      }
    }
    else // someone else has the ball
    {
      uint8_t ballTeam = game.ball.player / PLAYERS_IN_TEAM;
      uint8_t runAround = 1;
        
      dest[0] = p->position[0];
      dest[1] = p->position[1];

      if (ballTeam != myTeam && // enemy has the ball?
        game.ball.height == 0 && // don't go after goal keeper holding ball
        (distTaxi(p->position[0],p->position[1],
          game.ball.position[0],game.ball.position[1]) <= DIST_CLOSE) &&
        !areaHasAtLeastPlayers(game.ball.position[0],game.ball.position[1],
          SQUARE_SIZE,myTeam,1 + (p->aggressivity + p->ego) / 128))
      {
        if (distTaxi(p->position[0],p->position[1],
          game.ball.position[0],game.ball.position[1]) <= DIST_AT)
        {
          playerFightForBall(index);
          runAround = 0;
        }
        else if ((rnd() % 128) <
          ((unsigned int) (p->aggressivity + p->ego) / 2 + 25))
        {
          // go after him
          dest[0] = game.ball.position[0];
          dest[1] = game.ball.position[1];
          runAround = 0;
        }
      }

      if ((rnd() % 128 <= ((unsigned int) p->stamina + 20)) // not lazy?
        & runAround)
      {
        // find some spot near the action
        playerGetStartPos(index,dest);

        // if we have the ball, stretch the starting pos
        if (myTeam == game.ball.player / PLAYERS_IN_TEAM)
          dest[0] = myTeam ?
            (PITCH_W - (2 * (PITCH_W - dest[0]))) : (2 * dest[0]);

        dest[0] = (dest[0] + game.ball.position[0]) / 2; // average with ball X
      }

      playerWantsToRunTo(index,dest);
    }

    // closer to ball will react faster
    if (distTaxi(p->position[0],p->position[1],game.ball.position[0],
      game.ball.position[1]) < DIST_CLOSE)
      p->nextStateChange /= 2; 

    // sometimes randomly walk instead of run
    if (((p->stateDir & PLAYER_STATE_MASK) == PLAYER_STATE_RUNNING) &
      ((rnd() % 128) > p->stamina))
      p->stateDir = (p->stateDir & PLAYER_DIR_MASK) | PLAYER_STATE_WALKING;

    // lower IQ makes decisions less frequent
    p->nextStateChange += (128 - p->iq) / 8;
  }
  else // not changing state
  {
    if (state == PLAYER_STATE_RUNNING || state == PLAYER_STATE_WALKING)
    {
      uint8_t moveOnceIn = (ATTRIBUTE_MAX - p->speed) / 16;

      if (dir % 2)    // diagonal?
        moveOnceIn++; // move a bit slower

      moveOnceIn += moveOnceIn == 0; // prevent div by zero

      if (state == PLAYER_STATE_WALKING)
        moveOnceIn *= 2;

      if ((p->nextStateChange % moveOnceIn) == 0)
      {
        int16_t addVec[2];

        dirToVector(dir,addVec);

        p->position[0] += addVec[0];
        p->position[1] += addVec[1];

        if (p->position[0] >= PITCH_W - SQUARE_SIZE)
          p->position[0] = PITCH_W - SQUARE_SIZE - 1;
        else if (p->position[0] < SQUARE_SIZE)
          p->position[0] = SQUARE_SIZE;

        if (p->position[1] >= PITCH_H - SQUARE_SIZE)
          p->position[1] = PITCH_H - SQUARE_SIZE - 1;
        else if (p->position[1] < SQUARE_SIZE)
          p->position[1] = SQUARE_SIZE;
      }
    }

    // slowly shift goalies to the center of the goal
    if (game.ball.player != index &&
      (index == 0 || index == PLAYERS_IN_TEAM) &&
      game.playFrame % SAF_FPS == 0)
      p->position[1] += p->position[1] > PITCH_H / 2 ? -1 :
        (p->position[1] < PITCH_H / 2 ? 1 : 0);

    p->nextStateChange--;
  }
}

void gamePlayStep(void)
{
  if (game.nextTripIn == 0)
  {
    uint8_t playerIndex = rnd() % PLAYERS_TOTAL;

    // chance of tripping
    if ((rnd() % 128) >
      (game.players[playerIndex].agility +  
      game.players[playerIndex].stamina) / 2)
      playerTrip(playerIndex);

    game.nextTripIn = rndNorm(TRIP_TIME);
  }
  else
    game.nextTripIn--;
  
  for (int i = 0; i < PLAYERS_TOTAL; ++i)
    playerUpdate(i);

  ballUpdate();

  if (game.ball.position[0] < SQUARE_SIZE ||
    game.ball.position[0] > PITCH_W - SQUARE_SIZE ||
    game.ball.position[1] < SQUARE_SIZE ||
    game.ball.position[1] > PITCH_H - SQUARE_SIZE)
  {
    SAF_playSound(SOUND_WHISTLE);

    DEBUG_PRINT("ball out");

    if (game.ball.position[1] < SQUARE_SIZE ||
      game.ball.position[1] > PITCH_H - SQUARE_SIZE)
    {
      gameDisplayText("out");
      DEBUG_PRINT("throw in");

      game.playerToKick =
        findPlayerToKick(1 - game.ball.lastPlayer / PLAYERS_IN_TEAM);

      ballReset(game.ball.position[0],game.ball.position[1]);

      gameSetState(GAME_STATE_FREE_KICK);
    }
    else
    {
      int ballOut = game.ball.position[0] < SQUARE_SIZE;

      for (int i = 0; i < 2; ++i)
      {
        if (ballOut)
        {
          if (
            (game.ball.position[1] < PITCH_H / 2 - GOAL_WIDTH / 2) ||
            (game.ball.height > GOAL_HEIGHT &&
              game.ball.position[1] <= PITCH_H / 2) ||
            (game.ball.position[1] > PITCH_H / 2 + GOAL_WIDTH / 2) ||
              (game.ball.height > GOAL_HEIGHT &&
               game.ball.position[1] > PITCH_H / 2))
          {
            if (i == (game.ball.lastPlayer / PLAYERS_IN_TEAM))
            {
              gameDisplayText("corner");
              DEBUG_PRINT("corner");

              game.playerToKick =
                findPlayerToKick(1 - game.ball.lastPlayer / PLAYERS_IN_TEAM);

              game.ball.height = 0;
              game.ball.maxHeight = 0;

              game.ball.vecTo[0] =
                (i ? PITCH_W - SQUARE_SIZE - 1 : (SQUARE_SIZE + 1))
                - game.ball.position[0];

              game.ball.vecTo[1] = 
                ((game.ball.position[1] < PITCH_H / 2) ?
                (SQUARE_SIZE + 1) : (PITCH_H - SQUARE_SIZE - 1))
                - game.ball.position[1];

              gameSetState(GAME_STATE_FREE_KICK);
            }
            else
            {
              DEBUG_PRINT("kickoff");
              game.ball.player = i * PLAYERS_IN_TEAM; // give to the goal keeper
              gameSetState(GAME_STATE_KICKOFF);
            }
          }
          else if (game.ball.height == GOAL_HEIGHT ||
            (game.ball.position[1] == PITCH_H / 2 - GOAL_WIDTH / 2) ||
            (game.ball.position[1] == PITCH_H / 2 + GOAL_WIDTH / 2)
          )
          {
            SAF_playSound(SOUND_BOOM);
            gameDisplayText("bar!");
            DEBUG_PRINT("bar!");
            ballHorizontalBounce(i);
          }
          else
          {
            if (game.ball.lastPlayer >= 0 && 
              ((game.players[game.ball.lastPlayer].goalsFouls & 0x0f) != 0x0f))
              game.players[game.ball.lastPlayer].goalsFouls++;

            gameDisplayText("scores!");
            game.score[!i]++;
            gameSetState(GAME_STATE_AFTER_GOAL);
            DEBUG_PRINT("goal!");
            SAF_playSound(SOUND_BEEP);
          }
        }

        ballOut = game.ball.position[0] > PITCH_W - SQUARE_SIZE;
      }
    }
  }

  game.playFrame++;

  if (game.playFrame == PLAY_FRAMES / 2)
  {
    gameDisplayText("half time");
    SAF_playSound(SOUND_WHISTLE);
    gameSetState(GAME_STATE_HALFTIME);
    game.playFrame++;
  }
  
  if (game.playFrame == PLAY_FRAMES)
  {
    gameDisplayText("end!");
    SAF_playSound(SOUND_WHISTLE);
    gameSetState(GAME_STATE_END);
  }
}

void cameraUpdate(void)
{
  if (cameraFollowMode != 255)
  {
    for (int i = 0; i < 2; ++i)
    {
      int newPos =
        (cameraFollowMode < PLAYERS_TOTAL ?
          game.players[cameraFollowMode].position[i] :
          game.ball.position[i]);

      if (i == 0 && game.playFrame > PLAY_FRAMES / 2 + 1)
        newPos = PITCH_W - newPos;

      newPos -= 32;

      newPos = clamp(cameraPos[i],newPos - 4,newPos + 4);
      cameraPos[i] = (cameraPos[i] + newPos) / 2;
    }
  }
  else
  {
    if (SAF_buttonPressed(SAF_BUTTON_LEFT))
      cameraPos[0] -= CAMERA_SPEED;
    else if (SAF_buttonPressed(SAF_BUTTON_RIGHT))
      cameraPos[0] += CAMERA_SPEED;
    else if (SAF_buttonPressed(SAF_BUTTON_UP))
      cameraPos[1] -= CAMERA_SPEED;
    else if (SAF_buttonPressed(SAF_BUTTON_DOWN))
      cameraPos[1] += CAMERA_SPEED;
  }

  cameraPos[0] = clamp(cameraPos[0],-1 * SQUARE_SIZE,
    PITCH_W - SAF_SCREEN_WIDTH + SQUARE_SIZE);
  cameraPos[1] = clamp(cameraPos[1],-1 * SQUARE_SIZE,
    PITCH_H - SAF_SCREEN_HEIGHT + SQUARE_SIZE);
}

// Gets a menu item increment/decrement depending on buttons and time pressed.
int32_t menuItemIncrement(int32_t maxIncrement)
{
  uint8_t timePressed = SAF_buttonPressed(SAF_BUTTON_RIGHT) |
    SAF_buttonPressed(SAF_BUTTON_LEFT);

  int32_t result = SAF_buttonPressed(SAF_BUTTON_RIGHT) ? 1 : -1;

  if (timePressed < 2 * SAF_FPS)
    return (timePressed == 1 || timePressed > SAF_FPS) ? result : 0;

  if (timePressed == 255)
    return result * maxIncrement;

  maxIncrement /= 8;

  return result * (maxIncrement ? maxIncrement : 1);
}

void gameInit(uint32_t gameNumber)
{
  DEBUG_PRINT("init game");

  const char *country1 = gameNumToCountryCode(gameNumber,0);
  const char *country2 = gameNumToCountryCode(gameNumber,1);

  game.score[0] = 0;
  game.score[1] = 0;

  game.number = gameNumber;

  ballReset(PITCH_W / 2,PITCH_H / 2);

  gameDisplayText("");

  for (int i = 0; i < PLAYERS_TOTAL; ++i)
  {
    game.players[i] = playerGenerate(
      i < PLAYERS_IN_TEAM ? country1 : (
        i == PLAYERS_TOTAL - 1 ? "??" :  // referee
        country2),
        (country1[0] == country2[0] &&   // same countries -> make diff. teams
        country1[1] == country2[1]) ?
          i : (i % PLAYERS_IN_TEAM),gameNumToSeason(gameNumber));

    playerGetStartPos(i,game.players[i].position);
  }

  currentRand = gameNumToSeed(gameNumber);

  game.playFrame = 0;
  game.nextTripIn = rndNorm(TRIP_TIME);
  game.playerToKick = 0;

  gameSetState(GAME_STATE_AFTER_GOAL);
}

void gameUpdate(void)
{
  if (menuState != MENU_STATE_OFF)
  {
    if (SAF_buttonJustPressed(SAF_BUTTON_A) && menuState == 6)
    {
      gameInit(menuGameNumber);
      menuState = MENU_STATE_OFF;
    }
    if (SAF_buttonPressed(SAF_BUTTON_LEFT) ||
      SAF_buttonPressed(SAF_BUTTON_RIGHT))
    {
      switch (menuState)
      {
        case 0: /* fall through */ // countries
        case 1:
        {
          uint8_t country1 = gameNumToCountryNum(menuGameNumber,0);
          uint8_t country2 = gameNumToCountryNum(menuGameNumber,1);

          if (menuState == 0)
            country1 =
              (country1 + COUNTRIES + menuItemIncrement(1)) % COUNTRIES;
          else
            country2 =
              (country2 + COUNTRIES + menuItemIncrement(1)) % COUNTRIES;

          menuGameNumber = createGameNum(country1,country2,
            gameNumToSeason(menuGameNumber),gameNumToSeed(menuGameNumber));

          break;
        }

        case 2: // season
          menuGameNumber = createGameNum(
            gameNumToCountryNum(menuGameNumber,0),
            gameNumToCountryNum(menuGameNumber,1),
            gameNumToSeason(menuGameNumber) + menuItemIncrement(16),
            gameNumToSeed(menuGameNumber));
          break;

        case 3: // seed
          menuGameNumber = createGameNum(
            gameNumToCountryNum(menuGameNumber,0),
            gameNumToCountryNum(menuGameNumber,1),
            gameNumToSeason(menuGameNumber),
            gameNumToSeed(menuGameNumber) + menuItemIncrement(0x100));
          break;

        case 4: // game number
          menuGameNumber += menuItemIncrement(0x100000);
          break;

        case 5: // camera follow
          cameraFollowMode += menuItemIncrement(1);

          switch (cameraFollowMode)
          {
            case (CAMERA_FOLLOW_NONE - 1):
              cameraFollowMode = CAMERA_FOLLOW_BALL; break;

            case (CAMERA_FOLLOW_BALL - 1):
              cameraFollowMode = PLAYERS_TOTAL - 2; break;

            case (CAMERA_FOLLOW_BALL + 1):
              cameraFollowMode = CAMERA_FOLLOW_NONE; break;

            case (PLAYERS_TOTAL - 1):
              cameraFollowMode = CAMERA_FOLLOW_BALL; break;

            default: break;
          }

          break;

        default: break;
      }
    }
    else if (SAF_buttonJustPressed(SAF_BUTTON_DOWN))
    {
      menuState = (menuState + 1) % MENU_ITEMS;
      SAF_playSound(SAF_SOUND_CLICK);
    }
    else if (SAF_buttonJustPressed(SAF_BUTTON_UP))
    {
      menuState = (MENU_ITEMS + menuState - 1) % MENU_ITEMS;
      SAF_playSound(SAF_SOUND_CLICK);
    }
    else if (SAF_buttonJustPressed(SAF_BUTTON_B))
    {
      menuState = MENU_STATE_OFF;
      SAF_playSound(SAF_SOUND_BUMP);
    }

    return;
  }

  if (SAF_buttonJustPressed(SAF_BUTTON_B))
  {
    menuState = 0;
    menuGameNumber = game.number;
    SAF_playSound(SAF_SOUND_BUMP);
    return;
  }

  if (game.displayTextCountdown)
    game.displayTextCountdown--;
  else
    gameDisplayText("");

  switch (game.state)
  {
    case GAME_STATE_FREE_KICK:
      if (game.stateFrame < 2 * SAF_FPS)
      {
        int16_t dest[2];

        for (int i = 0; i < PLAYERS_TOTAL; ++i)
        {
          Player *p = game.players + i;

          if (i == 0 || i == PLAYERS_IN_TEAM)
          {
            // goal keeper shifts towards his place (for penalty)
            dest[0] = p->position[0];
            dest[1] = PITCH_H / 2;
          }
          else if (i == game.playerToKick)
          {
            dest[0] = game.ball.position[0] + game.ball.vecTo[0];
            dest[1] = game.ball.position[1] + game.ball.vecTo[1];
          }
          else if (
            distTaxi(p->position[0],p->position[1],
            game.ball.position[0] + game.ball.vecTo[0],
            game.ball.position[1] + game.ball.vecTo[1]) < (3 * DIST_CLOSE) / 4)
          {
            // get players away from the ball
            for (int j = 0; j < 2; ++j)
              dest[j] = (p->position[j] +
                game.players[(i / PLAYERS_IN_TEAM) *
                  PLAYERS_IN_TEAM].position[j]) / 2;

            dest[1] += i * (SQUARE_SIZE / 2); // space them apart
          }
          else
          {
            dest[0] = p->position[0];
            dest[1] = p->position[1];
          }

          if (game.stateFrame > SAF_FPS / 2)
          {
            if (p->position[0] == dest[0] && p->position[1] == dest[1])
              p->stateDir = (p->stateDir & PLAYER_DIR_MASK) |
                PLAYER_STATE_STANDING;
            else
            {
              p->stateDir =
                (p->stateDir & PLAYER_DIR_MASK) | PLAYER_STATE_RUNNING;
              p->nextStateChange++;

              dirToVector(pointsToDir(p->position,dest),dest);
              p->position[0] += dest[0];
              p->position[1] += dest[1];
            }
          }
        }
      }
      else if (game.stateFrame >= 5 * SAF_FPS)
      {
        ballReset(
          clamp(game.ball.position[0] + game.ball.vecTo[0],
            SQUARE_SIZE + 1,PITCH_W - SQUARE_SIZE - 1),
          clamp(game.ball.position[1] + game.ball.vecTo[1],
            SQUARE_SIZE + 1,PITCH_H - SQUARE_SIZE - 1));

        if (distTaxi(game.ball.position[0],game.ball.position[1],
          (game.playerToKick / PLAYERS_IN_TEAM) ? SQUARE_SIZE :
          (PITCH_W - SQUARE_SIZE),PITCH_H / 2) < DIST_CLOSE)
          playerShootOnGoal(game.playerToKick);
        else
          playerPass(game.playerToKick);

        SAF_playSound(SOUND_WHISTLE);

        gameSetState(GAME_STATE_PLAYING);
      }
      else
      {
        Player *p = game.players + game.playerToKick;

        for (int i = 0; i < 2; ++i)
        {
          game.ball.position[i] +=
            game.ball.vecTo[i] - (game.ball.vecTo[i] / 2);
          game.ball.vecTo[i] /= 2;
        }

        if (game.ball.position[1] < SQUARE_SIZE || // out?
          game.ball.position[1] > PITCH_H - SQUARE_SIZE)
        {
          game.ball.height = 7;
          p->stateDir = PLAYER_STATE_THROWING |
            (game.ball.position[1] < PITCH_H / 2 ? PLAYER_DIR_D : PLAYER_DIR_U);
        }
        else
          p->stateDir = PLAYER_STATE_STANDING |
            ((game.playerToKick / PLAYERS_IN_TEAM) ? PLAYER_DIR_L : PLAYER_DIR_R);

        p->position[0] = game.ball.position[0];
        p->position[1] = game.ball.position[1];
      }

      break;

    case GAME_STATE_KICKOFF:
      if (game.stateFrame > 4 * SAF_FPS)
        gameSetState(GAME_STATE_PLAYING);
      else if (game.stateFrame > 2 * SAF_FPS)
         for (int i = 0; i < 2; ++i)
           game.ball.position[i] = (game.ball.position[i] +
             game.players[game.ball.player].position[i]) / 2;

      break;

    case GAME_STATE_HALFTIME:
      if (game.stateFrame < 2 * SAF_FPS)
      {
        for (int i = 0; i < PLAYERS_TOTAL; ++i)
          game.players[i].stateDir = PLAYER_STATE_STANDING |
            (game.players[i].stateDir & PLAYER_DIR_MASK);
      }
      else if (game.stateFrame < 6 * SAF_FPS)
      {
        for (int i = 0; i < PLAYERS_TOTAL; ++i)
        {
          if (game.players[i].position[0] > PITCH_W / 2)
          {
            game.players[i].stateDir = PLAYER_STATE_RUNNING | PLAYER_DIR_L;      
            game.players[i].position[0]--;
          }
          else if (game.players[i].position[0] < PITCH_W / 2)
          {
            game.players[i].stateDir = PLAYER_STATE_RUNNING | PLAYER_DIR_R;      
            game.players[i].position[0]++;
          }
          else
            game.players[i].stateDir = PLAYER_STATE_STANDING | PLAYER_DIR_D;      

          game.players[i].nextStateChange++;

          game.ball.position[0] = (game.ball.position[0] + PITCH_W / 2) / 2;
          game.ball.position[1] = (game.ball.position[1] + PITCH_H / 2) / 2;
        }
      }
      else
      {
        ballReset(PITCH_W / 2,PITCH_H / 2);
        game.playFrame++;              // flip rendering
        gameSetState(GAME_STATE_AFTER_GOAL); // from now on it's like after a goal
      }

      break;

    case GAME_STATE_AFTER_GOAL:
      if (game.stateFrame < 2 * SAF_FPS)
      {
        if (game.ball.lastPlayer >= 0 &&
         (game.ball.lastPlayer / PLAYERS_IN_TEAM) != // if not own goal
         (game.ball.position[0] > PITCH_W / 2)
        )
          game.players[game.ball.lastPlayer].stateDir =
            PLAYER_STATE_THROWING | PLAYER_DIR_D; // celebrate
      }
      else if (game.stateFrame >= 7 * SAF_FPS)
      {
        ballReset(PITCH_W / 2,PITCH_H / 2);
        gameSetState(GAME_STATE_PLAYING);
        SAF_playSound(SOUND_WHISTLE);
      }
      else
      {
        int16_t dest[2];

        game.ball.position[0] = (game.ball.position[0] + PITCH_W / 2) / 2;
        game.ball.position[1] = (game.ball.position[1] + PITCH_H / 2) / 2;

        game.ball.height /= 2;

        for (int i = 0; i < PLAYERS_TOTAL; ++i)
        {
          Player *p = game.players + i;

          playerGetStartPos(i,dest);

          p->stateDir = PLAYER_STATE_STANDING |
            ((i < PLAYERS_IN_TEAM) ? PLAYER_DIR_R : PLAYER_DIR_L);

          if (p->position[0] != dest[0] || p->position[1] != dest[1])
          {
            dirToVector(pointsToDir(p->position,dest),dest);
            p->position[0] += dest[0];
            p->position[1] += dest[1];

            p->stateDir = (p->stateDir & PLAYER_DIR_MASK) | PLAYER_STATE_RUNNING;

            p->nextStateChange++;
          }
        }
      }

      break;

    case GAME_STATE_END:
    {
      uint8_t winTeam = (game.score[0] == game.score[1]) ? 255 :
        (game.score[1] > game.score[0]);

      for (int i = 0; i < PLAYERS_TOTAL; ++i)
        game.players[i].stateDir = PLAYER_DIR_D |
        (i / 8 == winTeam ? PLAYER_STATE_THROWING : PLAYER_STATE_STANDING);
 
      break;
    }

    default: // GAME_STATE_PLAYING
      gamePlayStep();

      break;
  }

  cameraUpdate();

  if (menuState == MENU_STATE_OFF && game.stateFrame < 4096)
    game.stateFrame++;
}

void drawPitchLine(int x, int y, int len, uint8_t horizontal)
{
  int *xx = horizontal ? &x : &y;
  int *yy = horizontal ? &y : &x;
  int incDec = 1;

  (*yy)--;

  x -= cameraPos[0];
  y -= cameraPos[1];

  if (*yy < 0 || *yy >= 64 || *xx >= 64 || *xx + len < 0)
    return;

  if (*xx + len >= 64)
    len = 64 - *xx;

  if (*xx < 0)
  {
    len += *xx;
    *xx = 0;
  }

  if ((x % 2 == y % 2) ==
    ((cameraPos[0] + 512) % 2 == (cameraPos[1] + 512) % 2))
  {
    *yy += 1;
    incDec = 0;
  }

  while (len)
  {
    SAF_drawPixel(x,y,COLOR_LINE);
    *xx += 1;

    *yy += incDec ? 1 : -1;
    incDec = !incDec;
    len--;
  }
}

void drawPlayer(uint8_t index, int16_t screenX, int16_t screenY, uint8_t flip)
{
  Player *p = game.players + index;
  const uint8_t *p1;
  uint8_t *p2 = tmpImage + 2;
  uint8_t shirtColor = index < PLAYERS_IN_TEAM ? COLOR_TEAM_A :
    (index == PLAYERS_IN_TEAM * 2 ? COLOR_REFEREE : COLOR_TEAM_B);
  uint8_t skinColor = p->look & RACE_BIT ? COLOR_SKIN_BLACK : COLOR_SKIN_WHITE;
  uint8_t hairColor = p->look & HAIR_BIT ? COLOR_HAIR_LIGHT : COLOR_HAIR_DARK;
  uint8_t shoeColor = p->look & SHOE_BIT ? COLOR_SHOES_2 : COLOR_SHOES_1;
  uint8_t transform = 0;
  uint8_t imgIndex = 0;
  uint8_t state = p->stateDir & PLAYER_STATE_MASK;
  uint8_t quantizedDir = ((p->stateDir & PLAYER_DIR_MASK) / 2) * 2;

  if (flip)
  {
    if (state == PLAYER_STATE_KICK_R)
      state = PLAYER_STATE_KICK_L;
    else if (state == PLAYER_STATE_KICK_L)
      state = PLAYER_STATE_KICK_R;

    if (quantizedDir == PLAYER_DIR_R)
      quantizedDir = PLAYER_DIR_L;
    else if (quantizedDir == PLAYER_DIR_L)
      quantizedDir = PLAYER_DIR_R;
  }

  switch (state | quantizedDir)
  {
    // standing:
    case (PLAYER_STATE_STANDING | PLAYER_DIR_U): imgIndex = 2; break;
    case (PLAYER_STATE_STANDING | PLAYER_DIR_D): imgIndex = 0; break;
    case (PLAYER_STATE_STANDING | PLAYER_DIR_R): transform = SAF_TRANSFORM_FLIP;
                                                 // fall through
    case (PLAYER_STATE_STANDING | PLAYER_DIR_L): imgIndex = 4; break;

    // running and kicking up and down:
    case (PLAYER_STATE_KICK_R  | PLAYER_DIR_D):  // fall through
    case (PLAYER_STATE_KICK_R  | PLAYER_DIR_U):  transform = SAF_TRANSFORM_FLIP;
                                                 // fall through
    case (PLAYER_STATE_WALKING | PLAYER_DIR_U):  // fall through
    case (PLAYER_STATE_WALKING | PLAYER_DIR_D):  // fall through
    case (PLAYER_STATE_RUNNING | PLAYER_DIR_U):  // fall through
    case (PLAYER_STATE_RUNNING | PLAYER_DIR_D):  // fall through
    case (PLAYER_STATE_KICK_L  | PLAYER_DIR_U):  // fall through
    case (PLAYER_STATE_KICK_L  | PLAYER_DIR_D):  // fall through
      imgIndex = 1 + 2 * (quantizedDir == PLAYER_DIR_U);

      if (state == PLAYER_STATE_RUNNING)
        transform = ((p->nextStateChange >> 2) & 0x01) ? SAF_TRANSFORM_FLIP : 0;
      else if (state == PLAYER_STATE_WALKING)
        transform = ((p->nextStateChange >> 3) & 0x01) ? SAF_TRANSFORM_FLIP : 0;

      break;

    // running and kicking left and right:
    case (PLAYER_STATE_KICK_R  | PLAYER_DIR_R): // fall through
    case (PLAYER_STATE_KICK_L  | PLAYER_DIR_R): // fall through
    case (PLAYER_STATE_RUNNING | PLAYER_DIR_R): // fall through
    case (PLAYER_STATE_WALKING | PLAYER_DIR_R): transform = SAF_TRANSFORM_FLIP;
                                                // fall through
    case (PLAYER_STATE_KICK_R  | PLAYER_DIR_L): // fall through
    case (PLAYER_STATE_KICK_L  | PLAYER_DIR_L): // fall through
    case (PLAYER_STATE_RUNNING | PLAYER_DIR_L): // fall through
    case (PLAYER_STATE_WALKING | PLAYER_DIR_L): // fall through
      imgIndex = 5 +
      (
        (state | quantizedDir) == (PLAYER_STATE_KICK_R | PLAYER_DIR_R) ||
        (state | quantizedDir) == (PLAYER_STATE_KICK_L | PLAYER_DIR_L) ||
        (state == PLAYER_STATE_RUNNING && ((p->nextStateChange >> 2) & 0x01)) ||
        (state == PLAYER_STATE_WALKING && ((p->nextStateChange >> 3) & 0x01))
      );

      break;

    // lying:
    case (PLAYER_STATE_LYING | PLAYER_DIR_D): imgIndex = 7; break;
    case (PLAYER_STATE_LYING | PLAYER_DIR_U): imgIndex = 8; break;
    case (PLAYER_STATE_LYING | PLAYER_DIR_R): transform = SAF_TRANSFORM_FLIP;
                                                 // fall through
    case (PLAYER_STATE_LYING | PLAYER_DIR_L): imgIndex = 9;  break;

    // throwing:
    case (PLAYER_STATE_THROWING | PLAYER_DIR_D): imgIndex = 10; break;
    case (PLAYER_STATE_THROWING | PLAYER_DIR_U): // fall through
    case (PLAYER_STATE_THROWING | PLAYER_DIR_R): // fall through
    case (PLAYER_STATE_THROWING | PLAYER_DIR_L): imgIndex = 11; break;

    default: break;
  }

  p1 = playerImages + imgIndex * (PLAYER_IMAGE_W * PLAYER_IMAGE_H);

  // copy to tmpImage with replaced colors:
  for (int i = 0; i < PLAYER_IMAGE_W * PLAYER_IMAGE_H; ++i)
  {
    switch (*p1)
    {
      case 0xf6: *p2 = skinColor; break;
      case 0x44: *p2 = hairColor; break;
      case 0xa4: *p2 = shirtColor; break;
      case 0x65: *p2 = shoeColor; break;
      default:   *p2 = *p1; break;
    }

    p1++;
    p2++;
  }

  SAF_drawImage(tmpImage,screenX,screenY,transform,0x75);
}

void drawBall(uint8_t flip)
{
  int16_t drawPos[2];

  drawPos[0] = (flip ? PITCH_W - game.ball.position[0] : game.ball.position[0])
    - cameraPos[0] - 1;

  drawPos[1] = game.ball.position[1] - cameraPos[1] - 1;

  if (drawPos[0] < -1 || drawPos[0] > SAF_SCREEN_WIDTH + 1 ||
    drawPos[1] < -1)
    return;

  if (drawPos[1] < SAF_SCREEN_HEIGHT + 1)
    SAF_drawImage(imageBallShadow,drawPos[0],drawPos[1],0,SAF_COLOR_RED);

  drawPos[1] -= game.ball.height + 1;

  if (drawPos[1] < SAF_SCREEN_HEIGHT + 1)
    SAF_drawImage(imageBall,drawPos[0],drawPos[1],0,SAF_COLOR_RED);
}

void drawGame(void)
{
  int8_t startX = -1 * ((cameraPos[0] + 512) % 8),
         startY = -1 * ((cameraPos[1] + 512) % 8);

  uint8_t flip = game.playFrame > (PLAY_FRAMES / 2) + 1;

#if SAF_PLATFORM_COLOR_COUNT > 2
  for (int y = 0; y < 9; ++y)
    for (int x = 0; x < 9; ++x)
      SAF_drawImage(imageGrass,startX + x * 8,startY + y * 8,0,0);
#else
  SAF_clearScreen(SAF_COLOR_WHITE);
#endif

  drawPitchLine(SQUARE_SIZE,SQUARE_SIZE,PITCH_W - 2 * SQUARE_SIZE,1);
  drawPitchLine(SQUARE_SIZE,SQUARE_SIZE,PITCH_H - 2 * SQUARE_SIZE,0);
  
  drawPitchLine(SQUARE_SIZE,PITCH_H - SQUARE_SIZE,
    PITCH_W - 2 * SQUARE_SIZE + 1,1);
  drawPitchLine(PITCH_W - SQUARE_SIZE,SQUARE_SIZE,
    PITCH_H - 2 * SQUARE_SIZE + 1,0);
  
  drawPitchLine(PITCH_W / 2,SQUARE_SIZE,PITCH_H - 2 * SQUARE_SIZE,0);
  
  drawPitchLine(SQUARE_SIZE,PITCH_H / 2 - PENALTY_AREA_H / 2,PENALTY_AREA_W,1);
  drawPitchLine(SQUARE_SIZE,PITCH_H / 2 + PENALTY_AREA_H / 2,PENALTY_AREA_W,1);
  drawPitchLine(SQUARE_SIZE + PENALTY_AREA_W,PITCH_H / 2 - PENALTY_AREA_H / 2,
    PENALTY_AREA_H,0);

  drawPitchLine(SQUARE_SIZE,PITCH_H / 2 - GOAL_AREA_H / 2,GOAL_AREA_W,1);
  drawPitchLine(SQUARE_SIZE,PITCH_H / 2 + GOAL_AREA_H / 2,GOAL_AREA_W,1);
  drawPitchLine(SQUARE_SIZE + GOAL_AREA_W,PITCH_H / 2 - GOAL_AREA_H / 2,
    GOAL_AREA_H,0);

  drawPitchLine(PITCH_W - SQUARE_SIZE - PENALTY_AREA_W,
    PITCH_H / 2 - PENALTY_AREA_H / 2,PENALTY_AREA_W,1);
  drawPitchLine(PITCH_W - SQUARE_SIZE - PENALTY_AREA_W,
    PITCH_H / 2 + PENALTY_AREA_H / 2,PENALTY_AREA_W,1);
  drawPitchLine(PITCH_W - SQUARE_SIZE - PENALTY_AREA_W,
    PITCH_H / 2 - PENALTY_AREA_H / 2,PENALTY_AREA_H,0);

  drawPitchLine(PITCH_W - SQUARE_SIZE - GOAL_AREA_W,
    PITCH_H / 2 - GOAL_AREA_H / 2,GOAL_AREA_W,1);
  drawPitchLine(PITCH_W - SQUARE_SIZE - GOAL_AREA_W,
    PITCH_H / 2 + GOAL_AREA_H / 2,GOAL_AREA_W,1);
  drawPitchLine(PITCH_W - SQUARE_SIZE - GOAL_AREA_W,
    PITCH_H / 2 - GOAL_AREA_H / 2,GOAL_AREA_H,0);

  int imgX, imgY;

  imgY = PITCH_H / 2 - 2 * SQUARE_SIZE + 2 - cameraPos[1];

  if (imgY < SAF_SCREEN_HEIGHT && imgY > -3 * SQUARE_SIZE)
    for (int i = 0; i < 2; ++i) // goals
    {
      imgX = (i ? PITCH_W - SQUARE_SIZE : 0) - cameraPos[0];

      if (imgX < SAF_SCREEN_WIDTH && imgX > -1 * SQUARE_SIZE)
        SAF_drawImage(imageGoal,imgX,imgY,i ? 0 : SAF_TRANSFORM_FLIP,0);
    }

  imgX = PITCH_W / 2 - cameraPos[0];
  imgY = PITCH_H / 2 - cameraPos[1];

  if (imgX < SAF_SCREEN_WIDTH + CENTER_CIRCLE_R && imgX > -1 * CENTER_CIRCLE_R &&
    imgY < SAF_SCREEN_HEIGHT + CENTER_CIRCLE_R && imgY > -1 * CENTER_CIRCLE_R)
    SAF_drawCircle(imgX,imgY,CENTER_CIRCLE_R,COLOR_LINE,0);

  // sort players by vertical position for correct drawing:

  {
    int swapped = 1;

    for (int j = 0; swapped && j < PLAYERS_TOTAL - 1; ++j)
    {
      swapped = 0;

      for (int i = 0; i < PLAYERS_TOTAL - 1 - j; ++i)
        if (game.players[playerSortArray[i]].position[1] > 
          game.players[playerSortArray[i + 1]].position[1])
        {
          uint8_t tmp = playerSortArray[i];
          playerSortArray[i] = playerSortArray[i + 1];
          playerSortArray[i + 1] = tmp;
          swapped = 1;
        }
    }
  }

  {
    int ballDrawn = 0;

    for (int i = 0; i < PLAYERS_TOTAL; ++i)
    {
      int16_t drawPos[2];

      Player *p = game.players + playerSortArray[i];

      drawPos[0] = (flip ? PITCH_W - p->position[0] : p->position[0])
        - cameraPos[0];
      drawPos[1] = p->position[1] - cameraPos[1];

      if (!ballDrawn && p->position[1] > game.ball.position[1])
      {
        drawBall(flip);
        ballDrawn = 1;
      }

      if (drawPos[0] > -1 * PLAYER_IMAGE_W / 2 &&
          drawPos[0] < SAF_SCREEN_WIDTH + PLAYER_IMAGE_W / 2 &&
          drawPos[1] > -1 * PLAYER_IMAGE_W / 2 &&
          drawPos[1] < SAF_SCREEN_HEIGHT + PLAYER_IMAGE_H)
        drawPlayer(playerSortArray[i],
          drawPos[0] - PLAYER_IMAGE_W / 2,
          drawPos[1] - PLAYER_IMAGE_H + PLAYER_IMAGE_W / 2,flip);
    }

    if (!ballDrawn)
      drawBall(flip);
  }

  // HUD:

  if (!SAF_buttonPressed(SAF_BUTTON_A))
  {
    char str[8];

    str[0] = (game.playFrame * 90) / PLAY_FRAMES; // minutes

    if (str[0] < 10)
    {
      str[0] = '0' + str[0];
      str[1] = 0;
      SAF_drawText(str,59,59,COLORTEXT_1,1);
    }
    else
    {
      str[1] = '0' + (str[0] % 10);
      str[0] = '0' + (str[0] / 10);
      str[2] = 0;
      SAF_drawText(str,54,59,COLORTEXT_1,1);
    }

    const char *country = gameNumToCountryCode(game.number,0);

    str[0] = country[0];
    str[1] = country[1];
    str[2] = ':';

    country = gameNumToCountryCode(game.number,1);

    str[3] = country[0];
    str[4] = country[1];
    str[5] = 0;

    SAF_drawText(str,19,1,COLORTEXT_1,1);

    str[2] = 0;

    for (int i = 0; i < 2; ++i)
    {
      str[0] = '0' + (game.score[i] >= 10 ? game.score[i] / 10 : game.score[i]);
      str[1] = (game.score[i] >= 10 ? '0' + game.score[i] % 10 : 0);
      SAF_drawText(str,i ? (game.score[1] < 10 ? 59 : 54) : 1,1,COLORTEXT_2,1);
    }

    SAF_drawText(game.displayText,1,59,COLORTEXT_2,1);
  }
}

void drawMenu(void)
{
  SAF_clearScreen(MENU_BACKGROUND_COLOR);
  int drawY;
  char str[16];

  str[15] = 0;

#if SAF_PLATFORM_COLOR_COUNT > 2
  SAF_drawCircle(10,12,28,SAF_COLOR_GRAY,1);
#endif

  if (menuState >= (MENU_ITEMS - 2))
  {
    SAF_drawRect(0,0,SAF_SCREEN_WIDTH,6,SAF_COLOR_BLUE,1);
    SAF_drawText("stats",1,1,SAF_COLOR_WHITE,1);

    const char *strPtr = gameNumToCountryCode(game.number,
      menuState == (MENU_ITEMS - 1));

    str[0] = strPtr[0];    
    str[1] = strPtr[1];    
    str[2] = 0;

    SAF_drawText(str,1,7,COLORTEXT_2,1);

    uint8_t stat = (SAF_frame() >> 6) % 13;

    switch (stat)
    {
      case 0:  strPtr = "   AGE"; break;
      case 1:  strPtr = "HEIGHT"; break;
      case 2:  strPtr = "WEIGHT"; break;
      case 3:  strPtr = "   EGO"; break;
      case 4:  strPtr = "AGGRES"; break;
      case 5:  strPtr = " SPEED"; break;
      case 6:  strPtr = "STRENG"; break;
      case 7:  strPtr = "STAMIN"; break;
      case 8:  strPtr = " ACCUR"; break;
      case 9:  strPtr = "AGILIT"; break;
      case 10: strPtr = "    IQ"; break;
      case 11: strPtr = " GOALS"; break;
      case 12: strPtr = " FOULS"; break;
      default: strPtr = 0; break;
    }

    SAF_drawText(strPtr,30,7,COLORTEXT_2,1);

    uint8_t playerIndex = (menuState == (MENU_ITEMS - 1)) * PLAYERS_IN_TEAM;
    Player *p = game.players + playerIndex;

    drawY = 13;

    str[3] = 0;

    for (int i = 0; i < PLAYERS_IN_TEAM; ++i)
    {
      uint8_t statePrev = p->stateDir;
      uint8_t statNum = 0;

      p->stateDir = PLAYER_STATE_STANDING | PLAYER_DIR_D;
      drawPlayer(playerIndex,1,drawY,0);
      p->stateDir = statePrev;

      SAF_drawText(p->name,7,drawY + 1,COLORTEXT_2,1);

      switch (stat)
      {
        case 0: statNum = p->ageYears; break;
        case 1: statNum = p->heightCm; break;
        case 2: statNum = p->weightKg; break;
        case 3: statNum = p->ego; break;
        case 4: statNum = p->aggressivity; break;
        case 5: statNum = p->speed; break;
        case 6: statNum = p->strength; break;
        case 7: statNum = p->stamina; break;
        case 8: statNum = p->accuracy; break;
        case 9: statNum = p->agility; break;
        case 10: statNum = p->iq; break;
        case 11: statNum = p->goalsFouls & 0x0f; break;
        case 12: statNum = p->goalsFouls >> 4; break;
        default: break;
      }

      str[0] = statNum >= 100 ? '0' + statNum / 100 : ' ';
      str[1] = statNum >= 10 ? '0' + (statNum / 10) % 10 : ' ';
      str[2] = '0' + statNum % 10;
      
      SAF_drawText(str,48,drawY + 1,COLORTEXT_1,1);

      playerIndex++;
      drawY += 6;
      p++;
    }
  }
  else
  {
    drawY = 3;

    for (int i = 0; i < MENU_ITEMS - 1; ++i)
    {
      switch (i)
      {
#define S(n,v) str[n] = v;

        case 0: // fall through
        case 1:
        {
          const char *c = gameNumToCountryCode(menuGameNumber,i);

          S(0,'t') S(1,'e') S(2,'a') S(3,'m') S(4,' ') S(5,'0' + i) S(6,' ')
          S(7,' ') S(8,c[0]) S(9,c[1]) S(10,0)
          break;
        }

        case 2:
        {
          uint8_t s = gameNumToSeason(menuGameNumber);
          S(0,'s') S(1,'e') S(2,'a') S(3,'s') S(4,'o') S(5,'n') S(6,' ')
          S(7,'0' + s / 100) S(8,'0' + (s / 10) % 10) S(9,'0' + s % 10) S(10,0)
          break;
        }

        case 3:
        {
          uint16_t seed = gameNumToSeed(menuGameNumber);
          S(0,'g') S(1,'a') S(2,'m') S(3,'e') S(4,' ')
          S(5,'0' + (seed / 10000)) S(6,'0' + (seed / 1000) % 10)
          S(7,'0' + (seed / 100) % 10) S(8,'0' + (seed / 10) % 10)
          S(9,'0' + seed % 10) S(10,0)
          break;
        }

        case 4:
        {
          S(0,'#') S(1, ' ')

          for (int j = 0; j < 8; ++j)
            S(2 + j,hexDigitToChar((menuGameNumber >> (28 - 4 * j)) & 0x0f))

          S(10,0)
          break;
        }

        case 5:
          S(0,'c') S(1,'a') S(2,'m') S(3,':') S(4,' ') S(5,' ')

          if (cameraFollowMode == CAMERA_FOLLOW_BALL)
          {
            S(6,'b') S(7,'a') S(8,'l') S(9,'l') S(10,0)
          }
          else if (cameraFollowMode == CAMERA_FOLLOW_NONE)
          {
            S(6,'n') S(7,'o') S(8,'n') S(9,'e') S(10,0)
          }
          else
          {
            S(6,' ') S(7,' ')
            S(8,'0' + (cameraFollowMode + 1) / 10)
            S(9,'0' + (cameraFollowMode + 1) % 10)
            S(10,0)
          }

          break;

        case 6:
          S(0,'n') S(1,'e') S(2,'w') S(3,0)
          break;

        default:
          str[0] = 's'; str[1] = 't'; str[2] = 'a'; str[3] = 't';
          str[4] = 's'; str[5] = 0;
          break;
      }

#undef S

      if (menuState == i)
        SAF_drawRect(0,drawY - 1,SAF_SCREEN_WIDTH,6,SAF_COLOR_BLUE,
        SAF_PLATFORM_COLOR_COUNT > 2);

      SAF_drawText(str,2,drawY,COLORTEXT_2,1);

      drawY += 7;
    }
  }
}

void draw(void)
{
  if (menuState != MENU_STATE_OFF)
    drawMenu();
  else
    drawGame();
}

#if DEBUG
void playerPrint(Player p)
{
  printf("player %s (skill: %d)\n",p.name,getPlayerSkill(&p));
  printf("  %d years, %s, %s hair, %s foot, shoes %c, %d cm, %d kg\n",
    p.ageYears,
    (p.look & RACE_BIT) ? "black" : "white",
    (p.look & HAIR_BIT) ? "blonde" : "dark",
    (p.look & FOOT_BIT) ? "left" : "right",
    (p.look & SHOE_BIT) ? '1' : '2',
    p.heightCm,p.weightKg);
  printf("  %d ego, %d aggress., %d IQ\n",p.ego,p.aggressivity,p.iq);
  printf("  speed, stamina, accuracy, agility, strength: %d %d %d %d %d\n",
    p.speed, p.stamina, p.accuracy, p.agility, p.strength);
  puts("------");
}

void countryPrint(const char *code)
{
  printf("============= country: %c%c =============\n",code[0],code[1]);

  for (int i = 0; i < PLAYERS_IN_TEAM; ++i)
    playerPrint(playerGenerate(code,i,1));
}

void runQuickMatch(uint32_t gameNumber)
{
  const char *country = gameNumToCountryCode(gameNumber,0);

  printf("================\nrunning match: %c%c ",country[0],country[1]);
  country = gameNumToCountryCode(gameNumber,1);
  printf("%c%c\n",country[0],country[1]);
  gameInit(gameNumber);
  menuState = MENU_STATE_OFF;

  while (game.state != GAME_STATE_END)
    gameUpdate();

  printf("================\nresult: %d %d\n",game.score[0],game.score[1]);
}

void runTournament(int season, int seed, int rounds)
{
  printf("================\nrunning tournament\n");

  uint8_t countryWins[COUNTRIES];
  uint8_t countryLosses[COUNTRIES];
  uint8_t countryGoals[COUNTRIES];
  uint8_t playerGoals[COUNTRIES * PLAYERS_IN_TEAM];
  uint8_t playerFouls[COUNTRIES * PLAYERS_IN_TEAM];
  unsigned int averageGoals = 0;

  for (int i = 0; i < COUNTRIES; ++i)
  {
    countryWins[i] = 0;
    countryLosses[i] = 0;
    countryGoals[i] = 0;
  }

  for (int i = 0; i < COUNTRIES * PLAYERS_IN_TEAM; ++i)
  {
    playerGoals[i] = 0;
    playerFouls[i] = 0;
  }

  for (int k = 0; k < rounds; ++k)
    for (int i = 0; i < COUNTRIES - 1; ++i)
      for (int j = i + 1; j < COUNTRIES; ++j)
      {
        runQuickMatch(createGameNum(i,j,season,seed + k));

        averageGoals += game.score[0] + game.score[1];

        countryGoals[i] += game.score[0];
        countryGoals[j] += game.score[1];

        if (game.score[0] > game.score[1])
        {
          countryWins[i]++;
          countryLosses[j]++;
        }
        else if (game.score[1] > game.score[0])
        {
          countryLosses[i]++;
          countryWins[j]++;
        }

        for (int k = 0; k < PLAYERS_IN_TEAM; ++k)
        {
          playerGoals[i * PLAYERS_IN_TEAM + k] +=
            game.players[k].goalsFouls & 0x0f;
          playerFouls[i * PLAYERS_IN_TEAM + k] +=
            game.players[k].goalsFouls >> 4;
        }

        for (int k = 0; k < PLAYERS_IN_TEAM; ++k)
        {
          playerGoals[j * PLAYERS_IN_TEAM + k] +=
            game.players[PLAYERS_IN_TEAM + k].goalsFouls & 0x0f;
          playerFouls[j * PLAYERS_IN_TEAM + k] +=
            game.players[PLAYERS_IN_TEAM + k].goalsFouls >> 4;
        }
      }

  printf("===== done =====\n");

  for (int i = 0; i < COUNTRIES; ++i)
    printf("%c%c: %d wins, %d losses, %d draws, %d goals\n",
      countryNames[i * 2],countryNames[i * 2 + 1],
      countryWins[i],countryLosses[i],
      rounds * (COUNTRIES - 1) - countryWins[i] -
      countryLosses[i],countryGoals[i]);

  unsigned int mostGoals = 0, mostFouls = 0;

  for (int i = 0; i < COUNTRIES * PLAYERS_IN_TEAM; ++i)
  {
    if (playerGoals[i] > playerGoals[mostGoals])
      mostGoals = i;

    if (playerFouls[i] > playerFouls[mostFouls])
      mostFouls = i;
  }

  averageGoals /= (((COUNTRIES - 1) * COUNTRIES) / 2) * rounds;

  printf(
    "average goals: %u\nmost goals: %d, %d (%d)\nmost fouls: %d, %d (%d)\n",
    averageGoals,mostGoals / PLAYERS_IN_TEAM,mostGoals % PLAYERS_IN_TEAM,
    playerGoals[mostGoals],
    mostFouls / PLAYERS_IN_TEAM,mostFouls % PLAYERS_IN_TEAM,
    playerFouls[mostFouls]);
}

#endif

void SAF_init(void)
{
  tmpImage[0] = PLAYER_IMAGE_W;
  tmpImage[1] = PLAYER_IMAGE_H;

  cameraFollowMode = CAMERA_FOLLOW_BALL;

  game.displayText[DISPLAY_TEXT_SIZE - 1] = 0;

  for (int i = 0; i < PLAYERS_TOTAL; ++i)
    playerSortArray[i] = i;

  gameInit(createGameNum(0,1,0,0));
  menuState = 1;
}

uint8_t SAF_loop(void)
{
  draw();
  gameUpdate();
  return 1;
}
