diff --git a/2048.c b/2048.c
new file mode 100644
index 0000000..52e4ab8
--- /dev/null
+++ b/2048.c
@@ -0,0 +1,212 @@
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <helianthus.h>
+
+
+#define CELLSIZE 128
+#define MAPSIZE 4
+
+
+typedef struct {
+  Group group;
+  int value;
+} Cell;
+
+
+Animation grid;
+Animation numbers[11];
+Cell map[MAPSIZE][MAPSIZE];
+double speed = 8;
+
+
+void setSprite(Cell *cell, double x, double y) {
+  if (cell->group)
+    groupDestroyEach(cell->group);
+  else
+    cell->group = createGroup();
+  Sprite sprite = createSpriteEx(x, y, 1, 1);
+  spriteSetAnimation(sprite, numbers[cell->value]);
+  groupAdd(cell->group, sprite);
+}
+
+
+int add(int dr, int dc) {
+  int count = 0;
+  for(int r = 0; r < MAPSIZE; ++r)
+    for(int c = 0; c < MAPSIZE; ++c)
+      if (map[r][c].value < 0)
+        ++count;
+  if (count == 0) return 0;
+
+  int i = randomNumber(0, count-1);
+  for(int r = 0; r < MAPSIZE; ++r)
+    for(int c = 0; c < MAPSIZE; ++c)
+      if (map[r][c].value < 0)
+        if (i-- == 0) {
+          map[r][c].value = 0;
+          int cc = dc > 0 ? -1 : (dc < 0 ? MAPSIZE : c);
+          int rr = dr > 0 ? -1 : (dr < 0 ? MAPSIZE : r);
+          setSprite(&map[r][c], cc + 0.5, rr + 0.5);
+          return 1;
+        }
+
+  return 0;
+}
+
+
+void fallStep(int *moves, Cell **prev, int *skip, Cell *curr, Cell *empty) {
+  if (curr->value < 0) {
+    ++(*skip);
+  } else
+  if (*prev && (*prev)->value == curr->value && curr->value < 10) {
+    for(int i = 0; i < groupGetCount(curr->group); ++i)
+      groupAdd((*prev)->group, groupGet(curr->group, i));
+    groupDestroy(curr->group);
+    curr->group = NULL;
+    curr->value = -1;
+    ++((*prev)->value);
+    ++(*skip);
+    ++(*moves);
+    *prev = NULL;
+  } else {
+    if (empty != curr) {
+      empty->group = curr->group;
+      empty->value = curr->value;
+      curr->group = NULL;
+      curr->value = -1;
+      ++(*moves);
+    }
+    *prev = empty;
+  }
+}
+
+
+void fall(int dr, int dc) {
+  Cell *prev;
+  int skip;
+  int moves = 0;
+
+  if (dr == 0) {
+    for(int r = 0; r < MAPSIZE; ++r) {
+      prev = NULL; skip = 0;
+      if (dc < 0)
+        for(int c = 0; c < MAPSIZE; ++c)
+          fallStep(&moves, &prev, &skip, &map[r][c], &map[r][c-skip]);
+      else
+      if (dc > 0)
+        for(int c = MAPSIZE-1; c >= 0; --c)
+          fallStep(&moves, &prev, &skip, &map[r][c], &map[r][c+skip]);
+    }
+  } else
+  if (dc == 0) {
+    for(int c = 0; c < MAPSIZE; ++c) {
+      prev = NULL; skip = 0;
+      if (dr < 0)
+        for(int r = 0; r < MAPSIZE; ++r)
+          fallStep(&moves, &prev, &skip, &map[r][c], &map[r-skip][c]);
+      else
+      if (dr > 0)
+        for(int r = MAPSIZE-1; r >= 0; --r)
+          fallStep(&moves, &prev, &skip, &map[r][c], &map[r+skip][c]);
+    }
+  }
+
+  if (moves > 0) add(dr, dc);
+}
+
+
+void init() {
+  for(int r = 0; r < MAPSIZE; ++r) {
+    for(int c = 0; c < MAPSIZE; ++c) {
+      map[r][c].group = NULL;
+      map[r][c].value = -1;
+    }
+  }
+
+  grid = createAnimationEx("data/sprite/2048/frame.png", TRUE, TRUE, TRUE);
+  numbers[ 0] = createAnimation("data/sprite/2048/2.png");
+  numbers[ 1] = createAnimation("data/sprite/2048/4.png");
+  numbers[ 2] = createAnimation("data/sprite/2048/8.png");
+  numbers[ 3] = createAnimation("data/sprite/2048/16.png");
+  numbers[ 4] = createAnimation("data/sprite/2048/32.png");
+  numbers[ 5] = createAnimation("data/sprite/2048/64.png");
+  numbers[ 6] = createAnimation("data/sprite/2048/128.png");
+  numbers[ 7] = createAnimation("data/sprite/2048/256.png");
+  numbers[ 8] = createAnimation("data/sprite/2048/512.png");
+  numbers[ 9] = createAnimation("data/sprite/2048/1024.png");
+  numbers[10] = createAnimation("data/sprite/2048/2048.png");
+
+  add(0, 0);
+  add(0, 0);
+}
+
+
+void draw() {
+  saveState();
+  zoom(CELLSIZE);
+
+  saveState();
+  noStroke();
+  fillTexture(grid, 0, 0, 1, 1, FALSE);
+  rect(0, 0, MAPSIZE, MAPSIZE);
+  restoreState();
+
+  if (keyWentDown("left"))  fall(0, -1);
+  if (keyWentDown("right")) fall(0,  1);
+  if (keyWentDown("up"))    fall(-1, 0);
+  if (keyWentDown("down"))  fall( 1, 0);
+
+  double dp = speed * worldGetFrameTime();
+  int score = 0;
+  for(int r = 0; r < MAPSIZE; ++r) {
+    for(int c = 0; c < MAPSIZE; ++c) {
+      Cell *cell = &map[r][c];
+      if (cell->group) {
+        score += (1 << (cell->value + 1));
+        int count = groupGetCount(cell->group);
+        int ready = count > 1;
+        double prevX = 0, prevY = 0;
+
+        double cx = c + 0.5;
+        double cy = r + 0.5;
+        for(int i = 0; i < count; ++i) {
+          Sprite sprite = groupGet(cell->group, i);
+          double x = spriteGetX(sprite);
+          double y = spriteGetY(sprite);
+          x = fabs(x - cx) <= dp ? cx : (x < cx ? x + dp : x - dp);
+          y = fabs(y - cy) <= dp ? cy : (y < cy ? y + dp : y - dp);
+          spriteSetXY(sprite, x, y);
+
+          if (i > 0 && (fabs(prevX - x) > 0.01 || fabs(prevY - y) > 0.01))
+            ready = 0;
+          prevX = x;
+          prevY = y;
+        }
+
+        if (ready)
+          setSprite(cell, prevX, prevY);
+      }
+    }
+  }
+
+  char buf[20];
+  sprintf(buf, "%d", score);
+  worldSetTitle(buf);
+
+  drawSprites();
+  restoreState();
+}
+
+
+int main() {
+  worldSetInit(&init);
+  worldSetDraw(&draw);
+  worldSetSize(MAPSIZE*CELLSIZE, MAPSIZE*CELLSIZE);
+  worldSetVariableFrameRate();
+  worldRun();
+  return 0;
+}
+
diff --git a/data-src/sprite/2048.xcf b/data-src/sprite/2048.xcf
new file mode 100644
index 0000000..a399e09
Binary files /dev/null and b/data-src/sprite/2048.xcf differ
diff --git a/data/sprite/2048/1024.png b/data/sprite/2048/1024.png
new file mode 100644
index 0000000..79c81ae
Binary files /dev/null and b/data/sprite/2048/1024.png differ
diff --git a/data/sprite/2048/128.png b/data/sprite/2048/128.png
new file mode 100644
index 0000000..a9fae65
Binary files /dev/null and b/data/sprite/2048/128.png differ
diff --git a/data/sprite/2048/16.png b/data/sprite/2048/16.png
new file mode 100644
index 0000000..6d4e6e6
Binary files /dev/null and b/data/sprite/2048/16.png differ
diff --git a/data/sprite/2048/2.png b/data/sprite/2048/2.png
new file mode 100644
index 0000000..1b5f05b
Binary files /dev/null and b/data/sprite/2048/2.png differ
diff --git a/data/sprite/2048/2048.png b/data/sprite/2048/2048.png
new file mode 100644
index 0000000..8ffeee5
Binary files /dev/null and b/data/sprite/2048/2048.png differ
diff --git a/data/sprite/2048/256.png b/data/sprite/2048/256.png
new file mode 100644
index 0000000..42b4936
Binary files /dev/null and b/data/sprite/2048/256.png differ
diff --git a/data/sprite/2048/32.png b/data/sprite/2048/32.png
new file mode 100644
index 0000000..5c48c9e
Binary files /dev/null and b/data/sprite/2048/32.png differ
diff --git a/data/sprite/2048/4.png b/data/sprite/2048/4.png
new file mode 100644
index 0000000..e4114a9
Binary files /dev/null and b/data/sprite/2048/4.png differ
diff --git a/data/sprite/2048/512.png b/data/sprite/2048/512.png
new file mode 100644
index 0000000..b4573cb
Binary files /dev/null and b/data/sprite/2048/512.png differ
diff --git a/data/sprite/2048/64.png b/data/sprite/2048/64.png
new file mode 100644
index 0000000..71f7fcc
Binary files /dev/null and b/data/sprite/2048/64.png differ
diff --git a/data/sprite/2048/8.png b/data/sprite/2048/8.png
new file mode 100644
index 0000000..4f87298
Binary files /dev/null and b/data/sprite/2048/8.png differ
diff --git a/data/sprite/2048/frame.png b/data/sprite/2048/frame.png
new file mode 100644
index 0000000..e29ce4e
Binary files /dev/null and b/data/sprite/2048/frame.png differ
diff --git a/fractal.c b/fractal.c
index 3cbaca5..be3a8b1 100644
--- a/fractal.c
+++ b/fractal.c
@@ -84,4 +84,5 @@ int main() {
   worldSetDraw(&draw);
   worldSetFrameRateEx(1, 100);
   worldRun();
+  return 0;
 }