Blob Blame History Raw

#include <img.h>
#include <snd.h>

#include <ctype.h>
#include <dirent.h>

#define MAXSIZE       40
#define MAXCOUNT (MAXSIZE*MAXSIZE)
#define VBASE          8 // must be even
#define HBASE         10 // must be even

#define LETTERSRATIO   7
#define MAXLETTERS   256
#define MAXSOUNDS     16
#define RMTIME       1.0
#define EXTIME       2.0
#define SNDRATE    44100


typedef struct {
  Image src;
  XImage *x;
} ImageEx;

typedef struct {
  ImageEx a, b;
} ImagePair;

typedef struct {
  ImagePair glyphs[4];
  int glyphsCount;
  Sound sounds[MAXSOUNDS];
  int soundsCount;
  ImagePair *g[4];
} Letter;

typedef struct {
  int r, c;
} Pos;

typedef struct {
  int len;
  Pos p[4];
} Track;

typedef struct {
  Letter *l;
  ImagePair *g;
} Cell;


int cols = 4;
int rows = 4;
int count = 4*4;

Letter menuLetter;
Letter letters[MAXLETTERS];
int lettersCount;
int baseCellWidth = 1;
int baseCellHeight = 1;
int cellWidth = 1;
int cellHeight = 1;

Cell board[MAXSIZE][MAXSIZE];
Pos hover = { -1, -1 };
Pos selected = { -1, -1 };
Track tracks[MAXCOUNT];
int tracksCount;
int glyphsMask = 0;
int mode;
Letter *hintLetter;
int back;
int mute;
int ox, oy, cw, ch;

ImageEx bgtex;
ImageEx starttex;
ImageEx hinttex;
ImageEx backtex[3];
ImageEx sndtex[3];


void imgExFree(ImageEx *img) {
  if (!img) return;
  if (img->x) XDestroyImage(img->x);
  imgFree(&img->src);
  *img = (ImageEx){};
}


void imgExResample(ImageEx *img, int w, int h) {
  if (!imgValid(img->src) || w <= 0 || h <= 0) return;
  if (img->x && (img->x->width != w || img->x->height != h))
    { XDestroyImage(img->x); img->x = NULL; }
  if (!img->x) {
    Image tmp = imgResample(img->src, w, h);
    imgDivA(tmp);
    img->x = imgToX(&tmp);
  }
}


void imgExDraw(ImageEx *img, int x, int y, int w, int h) {
  imgExResample(img, w, h);
  if (img->x)
    XPutImage(dpy, drw, gc, img->x, 0, 0, x, y, img->x->width, img->x->height);
}


int imgExLoadTga(ImageEx *img, const char *filename) {
  imgExFree(img);
  img->src = imgLoadTga(filename);
  imgMultA(img->src);
  return imgValid(img->src);
}


void imgPairFree(ImagePair *img) {
  if (!img) return;
  imgExFree(&img->a);
  imgExFree(&img->b);
}


void imgPairDraw(ImagePair *img, int mode, int x, int y, int w, int h) {
  imgExResample(mode ? &img->a : &img->b, w, h);
  imgExDraw(mode ? &img->b : &img->a, x, y, w, h);
}


void clearBoard() {
  if (mute != 1) mute = 0;
  mode = 0;
  back = 0;
  hintLetter = NULL;
  memset(tracks, 0, sizeof(tracks));
  memset(board, 0, sizeof(board));
  tracksCount = 0;
  selected = (Pos){ -1, -1 };
}


void clearSounds() {
  for(int i = 0; i < lettersCount; ++i)
    for(int j = 0; j < letters[i].soundsCount; ++i)
      sndFree(&letters[i].sounds[j]);
}


void clearLetters() {
  clearBoard();
  clearSounds();
  for(int i = 0; i < lettersCount; ++i)
    for(int j = 0; j < letters[i].glyphsCount; ++j)
      imgPairFree(&letters[i].glyphs[j]);
  memset(letters, 0, sizeof(letters));
  memset(&menuLetter, 0, sizeof(menuLetter));
  lettersCount = 0;
  cellWidth = cellHeight = 1;
}


int loadLetters(const char *filename, const char *filesel, int rows, int cols) {
  clearLetters();

  Image img = imgLoadTga(filename);
  if (!imgValid(img))
    return 0;
  int ww = img.w/cols/2;
  int hh = img.h/rows/2;
  if (!ww || !hh)
    return LOGERR("loadLetters: image too small (%dx%d): %s", img.w, img.h, filename), imgFree(&img), 0;

  Image imgsel = imgLoadTga(filesel);
  if (!imgValid(imgsel))
    return imgFree(&img), 0;
  int wws = imgsel.w/cols/2;
  int hhs = imgsel.h/rows/2;
  if (!wws || !hhs)
    return LOGERR("loadLetters: image too small (%dx%d): %s", imgsel.w, imgsel.h, filesel), imgFree(&img), imgFree(&imgsel), 0;

  imgMultA(img);
  Letter *letter = letters;
  for(int r = 0; r < rows; ++r)
  for(int c = 0; c < cols; ++c)
  {
    for(int rr = 0; rr < 2; ++rr)
    for(int cc = 0; cc < 2; ++cc) {
      Image sub = imgSub(img, (c*2 + cc)*ww, (r*2 + rr)*hh, ww, hh);
      if (!imgHasPicture(sub))
        { imgFree(&sub); continue; }
      letter->glyphs[ letter->glyphsCount ].a.src = sub;
      letter->glyphs[ letter->glyphsCount ].b.src = imgSub(imgsel, (c*2 + cc)*wws, (r*2 + rr)*hhs, wws, hhs);
      ++letter->glyphsCount;
    }
    if (letter->glyphsCount > menuLetter.glyphsCount) menuLetter = *letter;
    if (letter->glyphsCount > 0) ++letter;
  }
  lettersCount = letter - letters;
  baseCellWidth = wws;
  baseCellHeight = hhs;
  cellWidth = ww;
  cellHeight = hh;
  assert(menuLetter.glyphsCount == 4);

  imgFree(&img);
  return 1;
}


void loadSounds(const char *path) {
  DIR *dir = opendir(path);
  if (!dir) return;
  size_t pl = strlen(path);

  char buf[1024];
  while(1) {
    struct dirent *e = readdir(dir);
    if (!e) break;
    size_t nl = strlen(e->d_name);
    if (nl < 4 || pl + nl + 1 >= sizeof(buf)) continue;

    const char *ex = e->d_name + nl - 4;
    if ( ex[0] != '.'
     || tolower(ex[1]) != 'r'
     || tolower(ex[2]) != 'a'
     || tolower(ex[3]) != 'w' ) continue;

    int index = atoi(e->d_name);
    if (index <= 0 || index > lettersCount) continue;

    Letter *l = &letters[index-1];
    if (l->soundsCount >= MAXSOUNDS) continue;

    sprintf(buf, "%s/%s", path, e->d_name);
    l->sounds[l->soundsCount] = sndLoadRaw(buf, SNDRATE);
    //l->sounds[l->soundsCount] = sndGen(SNDRATE, 100, SNDRATE);
    if (sndValid(l->sounds[l->soundsCount])) ++l->soundsCount;
  }
  closedir(dir);
}


void shuffleLetters(int glyps) {
  for(int i = 0; i < lettersCount; ++i) {
    int j = rand()%(lettersCount - i) + i;
    if (j != i) { Letter l = letters[i]; letters[i] = letters[j]; letters[j] = l; }
    Letter *l = &letters[i];
    assert(l->glyphsCount > 0);

    int gc = 0;
    ImagePair *g[4] = {};
    for(int j = 0; j < l->glyphsCount; ++j)
      if (glyps & (1 << j)) g[gc++] = &l->glyphs[j];
    if (!gc) g[gc++] = &l->glyphs[0];

    for(int j = 0; j < gc; ++j) {
      int k = rand()%(gc - j) + j;
      if (k != j) { ImagePair *a = g[j]; g[j] = g[k]; g[k] = a; }
    }

    for(int j = 0; j < 4; ++j)
      l->g[j] = g[j%gc];

    for(int j = 0; j < 4; ++j) {
      int k = rand()%(4 - j) + j;
      if (k != j) { ImagePair *g = l->g[j]; l->g[j] = l->g[k]; l->g[k] = g; }
      assert(l->g[j]);
    }
  }
}


void calcSize() {
    if (winW*baseCellHeight < baseCellWidth*winH) {
        cols = HBASE; // portrait
        rows = winH*cols*baseCellWidth/(winW*baseCellHeight);
    } else {
        rows = VBASE; // landscape
        cols = winW*rows*baseCellHeight/(winH*baseCellWidth);
    }
    cols -= 4; // place for buttons
    rows -= 2;
    if (rows < 1) rows = 1;
    if (cols < 1) cols = 1;
    if (rows > MAXSIZE) rows = MAXSIZE;
    if (cols > MAXSIZE) cols = MAXSIZE;
    count = rows*cols;
}


void simpleBoard() {
  clearBoard();
  calcSize();
  for(int i = 0; i < count; ++i) {
    Letter *l = &letters[i%lettersCount];
    board[i/cols][i%cols].l = l;
    board[i/cols][i%cols].g = &l->glyphs[1];
  }
  mode = 1;
}


void generateBoard(int glyphsMask) {
  clearBoard();
  calcSize();
  shuffleLetters(glyphsMask);

  int indices[MAXCOUNT];
  for(int i = 0; i < count; ++i) indices[i] = i;

  int maxLetters = count/LETTERSRATIO + 1;
  int lc = lettersCount < maxLetters ? lettersCount : maxLetters;
  for(int i = 0; i < count; ++i) {
    int j = rand()%(count - i) + i;
    int id = indices[j]; indices[j] = indices[i]; indices[i] = id;
    Letter *l = &letters[ (id/4)%lc ];
    board[i/cols][i%cols].l = l;
    board[i/cols][i%cols].g = l->g[id%4];
  }

  mode = 1;
}


Letter* get(Pos p)
  { return p.r >= 0 && p.r < rows && p.c >= 0 && p.c < cols ? board[p.r][p.c].l : NULL; }


int findSubTrack1(Track *t, Pos b) {
  Pos p = t->p[t->len - 1];
  //printf(" --  -- ft1: %d %d -> %d %d\n", p.r, p.c, b.r, b.c);
  if (p.c == b.c && p.r < b.r) { for(++p.r; p.r != b.r; ++p.r) if (get(p)) return 0; } else
  if (p.c == b.c && p.r > b.r) { for(--p.r; p.r != b.r; --p.r) if (get(p)) return 0; } else
  if (p.r == b.r && p.c < b.c) { for(++p.c; p.c != b.c; ++p.c) if (get(p)) return 0; } else
  if (p.r == b.r && p.c > b.c) { for(--p.c; p.c != b.c; --p.c) if (get(p)) return 0; } else return 0;
  t->p[t->len++] = p;
  return 1;
}


int findSubTrack2(Track *t, Pos b, int horz) {
  Pos *p = &t->p[t->len];
  *p = t->p[t->len - 1];

  //printf(" -- ft2: %d %d -> %d %d %s\n", p->r, p->c, b.r, b.c, horz ? "horz" : "vert");
  if (p->c == b.c || p->r == b.r) return findSubTrack1(t, b);


  ++t->len;
  if (horz) {
    if (p->c < b.c) { for(++p->c; p->c != b.c; ++p->c) if (get(*p)) break; }
               else { for(--p->c; p->c != b.c; --p->c) if (get(*p)) break; }
  } else {
    if (p->r < b.r) { for(++p->r; p->r != b.r; ++p->r) if (get(*p)) break; }
               else { for(--p->r; p->r != b.r; --p->r) if (get(*p)) break; }
  }
  if (!get(*p) && findSubTrack1(t, b)) return 1;
  --t->len;

  return 0;
}


int findSubTrack3(Track *t, Pos b) {
  Pos p0 = t->p[t->len - 1];
  Pos *p = &t->p[t->len];
  if (p->c == b.c && p->r == b.r) return 0;

  //printf("ft3: %d %d -> %d %d\n", p0.r, p0.c, b.r, b.c);
  if (findSubTrack2(t, b, 0)) return 1;

  ++t->len;

  *p = p0;
  for(++p->c; p->c <= cols; ++p->c)
    if (get(*p)) break; else
      if (findSubTrack2(t, b, 0)) return 1;

  *p = p0;
  for(--p->c; p->c >= -1; --p->c)
    if (get(*p)) break; else
      if (findSubTrack2(t, b, 0)) return 1;

  *p = p0;
  for(++p->r; p->r <= rows; ++p->r)
    if (get(*p)) break; else
      if (findSubTrack2(t, b, 1)) return 1;

  *p = p0;
  for(--p->r; p->r >= -1; --p->r)
    if (get(*p)) break; else
      if (findSubTrack2(t, b, 1)) return 1;

  --t->len;
  return 0;
}


Track findTrack(Pos a, Pos b) {
  Track t = {};
  t.p[t.len++] = a;
  Letter *la = get(a);
  Letter *lb = get(b);
  return la && la == lb && (a.c != b.c || a.r != b.r) && findSubTrack3(&t, b) ? t : (Track){};
}


void hint() {
  if (tracksCount) return;

  for(int i = 0; i < count; ++i) {
    Pos a = { i/cols, i%cols };
    for(int j = 0; j < count; ++j) {
      Pos b = { j/cols, j%cols };
      Track t = findTrack(a, b);
      if (t.len)
        tracks[tracksCount++] = t;
    }
  }

  LOGDBG("hint: tracks count: %d", tracksCount);
  if (!tracksCount) return; // beep

  int i = rand()%tracksCount;
  if (i) tracks[0] = tracks[i];
  tracksCount = 1;
}


int init() {
  sndInit();
  srand(time(NULL));
  imgExLoadTga(&bgtex,      "data/minas.tga");
  imgExLoadTga(&starttex,   "data/start.tga");
  imgExLoadTga(&hinttex,    "data/hint.tga");
  imgExLoadTga(&backtex[0], "data/back.tga");
  imgExLoadTga(&backtex[1], "data/back1.tga");
  imgExLoadTga(&backtex[2], "data/back2.tga");
  imgExLoadTga(&sndtex[0],  "data/sound.tga");
  imgExLoadTga(&sndtex[1],  "data/silence.tga");
  imgExLoadTga(&sndtex[2],  "data/sound2.tga");
  if (!loadLetters("data/letters.tga", "data/letters-sel.tga", 5, 8))
    return LOGERR("init: cannot load letters"), 0;
  loadSounds("data/sounds");
  //simpleBoard();
  return 1;
}


void deinit() {
  clearLetters();
  imgExFree(&bgtex);
  imgExFree(&starttex);
  imgExFree(&hinttex);
  imgExFree(&backtex[0]);
  imgExFree(&backtex[1]);
  imgExFree(&backtex[2]);
  imgExFree(&sndtex[0]);
  imgExFree(&sndtex[1]);
  imgExFree(&sndtex[2]);
  sndDeinit();
}


void resize() { }


void mouseDown(int x, int y) {
  int mc = (x - ox)/cw;
  int mr = (y - oy)/ch;
  if (x < ox) --mc;
  if (y < oy) --mr;

  if (mode == 0) {
    if (!mr && mc >= 0 && mc < 4) {
      glyphsMask ^= 1 << mc;
    } else
    if (mr == 1 && mc >= 1 && mc <= 2) {
      generateBoard(glyphsMask);
    }
  } else {
    if (mc < 0 && mr < 0) {
      if (++back >= 3) clearBoard();
      return;
    } else back = 0;

    hover.r = hover.c = -1;
    hintLetter = NULL;
    tracksCount = 0;
    for(int r = 0; r < rows; ++r)
    for(int c = 0; c < cols; ++c)
      if (!board[r][c].l) board[r][c].g = NULL;

    if (mc >= cols && mr >= rows) {
      hintLetter = get(selected);
      if (!hintLetter) hint();
      selected.r = selected.c = -1;
    } else
    if (mc >= cols && mr < 0) {
      mute = mute == 1 ? 2 : 1;
    } else
    if (mc >= 0 && mr >= 0 && mc < cols && mr < rows) {
      hover.r = mr;
      hover.c = mc;
      Letter *l = get(hover);
      if (l && l->soundsCount > 0 && mute != 1)
        sndPlay(l->sounds[rand() % l->soundsCount]);
      if (!l || (mr == selected.r && mc == selected.c)) {
        selected.r = selected.c = hover.r = hover.c = -1;
      } else
      if (l == get(selected)) {
        Track t = findTrack(selected, hover);
        if (t.len) {
          Cell *ca = &board[selected.r][selected.c];
          Cell *cb = &board[hover.r][hover.c];
          ca->l = cb->l = NULL;
          tracks[tracksCount++] = t;
        } else {
          selected = hover;
        }
      } else {
        selected = hover;
      }
    } else {
      selected.r = selected.c = hover.r = hover.c = -1;
    }
  }
}


void mouseUp() {
  int cnt = 0;
  for(int r = 0; r < rows; ++r)
  for(int c = 0; c < cols; ++c)
    if (!board[r][c].l && board[r][c].g)
      { ++cnt; board[r][c].g = NULL; }
  if (cnt) tracksCount = 0;
}


void draw() {
  if (imgValid(bgtex.src)) {
    if (winW*bgtex.src.h > winH*bgtex.src.w) {
      int h = bgtex.src.h*winW/bgtex.src.w;
      imgExDraw(&bgtex, 0, (winH - h)/2, winW, h);
    } else {
      int w = bgtex.src.w*winH/bgtex.src.h;
      imgExDraw(&bgtex, (winW - w)/2, 0, w, winH);
    }
  }

  if (mode == 0) {
    if (winW*cellHeight*3 < winH*cellWidth*6) {
      cw = winW/6;
      ch = cellHeight*cw/cellWidth;
    } else {
      ch = winH/3;
      cw = cellWidth*ch/cellHeight;
    }
    ox = (winW - cw*4)/2;
    oy = (winH - ch)/2;

    for(int i = 0; i < 4; ++i)
      imgPairDraw(&menuLetter.glyphs[i], !(glyphsMask & (1 << i)), ox + i*cw, oy, cw, ch);
    if (glyphsMask)
      imgExDraw(&starttex, ox + cw, oy + ch, 2*cw, ch);
  } else {
    if (winW*cellHeight*(rows + 2) < winH*cellWidth*(cols + 4)) {
      cw = winW/(cols + 4);
      ch = cellHeight*cw/cellWidth;
    } else {
      ch = winH/(rows + 2);
      cw = cellWidth*ch/cellHeight;
    }
    ox = (winW - cw*cols)/2;
    oy = (winH - ch*rows)/2;

    // draw buttons
    imgExDraw(&backtex[back], ox - 2*cw, oy - ch, 2*cw, ch);
    imgExDraw(&hinttex, ox + cols*cw, oy + rows*ch, 2*cw, ch);
    imgExDraw(&sndtex[mute], ox + cols*cw, oy - ch, 2*cw, ch);
    for(int i = 0; i < 3; ++i) imgExResample(&sndtex[i], cw, ch);

    // draw cells
    for(int r = 0; r < rows; ++r)
    for(int c = 0; c < cols; ++c) {
      Cell *cell = &board[r][c];
      if (!cell->g) continue;
      int hilight = (r == hover.r && c == hover.c)
                 || (r == selected.r && c == selected.c)
                 || (hintLetter && hintLetter == cell->l);
      imgPairDraw(cell->g, hilight, ox + c*cw, oy + r*ch, cw, ch);
    }

    // draw tracks
    int lw = cw/6;
    if (lw < 1) lw = 1;
    XSetForeground(dpy, gc, 0xff0000);
    XSetLineAttributes(dpy, gc, lw, LineSolid, CapRound, JoinRound);
    for(int i = 0; i < tracksCount; ++i) {
      Track *t = &tracks[i];
      XPoint p[4];
      for(int j = 0; j < t->len; ++j) {
        p[j].x = ox + t->p[j].c*cw + cw/2;
        p[j].y = oy + t->p[j].r*ch + ch/2;
      }
      XDrawLines(dpy, drw, gc, p, t->len, CoordModeOrigin);
    }
  }
}