Classic Solitaire Klondike

Great!

I mean the part you just added, with the trails, when you win.

1 Like

At the end cards fall down one stack at a time, maybe make it all four altogether? Should look fancier.

When card is picked, make it bigger (scale up) and add a shadow underneath. That will make an illusion of a raised card above the table. The same technique can be used when a card is flipped, and since Defold is 3D, make a proper 3D rotation with perspective (when one side gets bigger during the rotation).

Add a texture background that looks more like a textile and some slight dark gradient to make a feel of a lamp nearby.

Cards transition linearly? Some easing could also make it look snappier.

No lose state determination?

3 Likes

Many of this things already in to-do list.
We have no plans for monetization of this game - we don’t want to spend many time for it. Time to time I will make some features and improvements, only when I have no any other things.
Thank you for your feedback and suggestions.

5 Likes

One more programmer joined to our team, which means that we will be able to do a little more =)
Our solitaire was updated to version 1.55
What’s new:

  • The game now can be played in landscape orientation (requested by @ross.grams =) )
  • Added button “Auto win” in case if all the cards are face up.
  • Reduced the size of the build.
  • Minor bugfixes.

Android and HTML5 version already updated. IOS build - Waiting for Review.

6 Likes

How much did you manage to reduce the size, and how?

The build size reduced using buildmanifest:

# Release + No: Physics + Record + Profiler + Facebook
platforms:

    js-web:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

    armv7-android:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

    armv7-ios:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]
            
    arm64-ios:
        context:
            excludeLibs: ["BulletDynamics", "BulletCollision", "LinearMath", "Box2D", "physics", "record", "vpx", "engine", "profilerext"]
            excludeSymbols: ["FacebookExt", "ProfilerExt"]
            libs: ["engine_release", "physics_null", "record_null"]

iOS:

Android:
OLD Version: 3.06 Mb
NEW Version: 2.58 Mb

Html5 gzip:
OLD Version :


NEW Version:

15 Likes

Wow, fantastic! :smiley:

1 Like

I just received few crash reports, maybe you can help, pls?

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'Huawei/ALE-L23/hwALE-H:5.0.1/HuaweiALE-L23/C605B150:user/release-keys'
Revision: '0'
ABI: 'arm'
pid: 32342, tid: 32433, name: Thread-303  >>> com.potatojam.classic.solitaire.klondike <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '../src/opengl/graphics_opengl.cpp:1646: void dmGraphics::SetTexture(dmGraphics::HTexture, const dmGraphics::TextureParams&): assertion "params.m_Height <= g_Context->m_MaxTextureSize" failed'
    r0 00000000  r1 00007eb1  r2 00000006  r3 00000000
    r4 e302bdd8  r5 00000006  r6 00000002  r7 0000010c
    r8 e302b764  r9 e302b740  sl e3291adc  fp ab545438
    ip 00007eb1  sp e302b5e8  lr f707d699  pc f70a3a94  cpsr 600f0010
backtrace:
    #00 pc 0003ca94  /system/lib/libc.so (tgkill+12)
    #01 pc 00016695  /system/lib/libc.so (pthread_kill+52)
    #02 pc 000172a7  /system/lib/libc.so (raise+10)
    #03 pc 00013be5  /system/lib/libc.so (__libc_android_abort+36)
    #04 pc 000123bc  /system/lib/libc.so (abort+4)
    #05 pc 00014dc3  /system/lib/libc.so (__libc_fatal+16)
    #06 pc 00013c69  /system/lib/libc.so (__assert2+20)
    #07 pc 000d795c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmGraphics::SetTexture(dmGraphics::Texture*, dmGraphics::TextureParams const&)+496)
    #08 pc 000d900c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmGraphics::NewRenderTarget(dmGraphics::Context*, unsigned int, dmGraphics::TextureCreationParams const*, dmGraphics::TextureParams const*)+612)
    #09 pc 000e683c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (dmRender::RenderScript_RenderTarget(lua_State*)+1512)
    #10 pc 0010f4f8  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so
Tombstone written to: /data/tombstones/tombstone_00


logcat.txt.zip (148.1 KB)

And second:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'samsung/j1acevelteub/j1acevelte:5.1.1/LMY47V/J111MUBU0AQF2:user/release-keys'
Revision: '4'
ABI: 'arm'
pid: 21771, tid: 21853, name: Thread-1856  >>> com.potatojam.classic.solitaire.klondike <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: '../src/opengl/graphics_opengl.cpp:1646: void dmGraphics::SetTexture(dmGraphics::HTexture, const dmGraphics::TextureParams&): assertion "params.m_Height <= g_Context->m_MaxTextureSize" failed'
    r0 00000000  r1 0000555d  r2 00000006  r3 00000000
    r4 a2dcfdb8  r5 00000006  r6 00000009  r7 0000010c
    r8 a2dcf744  r9 a2dcf720  sl a302eadc  fp b4772e80
    ip 0000555d  sp a2dcf5c8  lr b6de3735  pc b6e06dbc  cpsr 600f0010
backtrace:
    #00 pc 0003adbc  /system/lib/libc.so (tgkill+12)
    #01 pc 00017731  /system/lib/libc.so (pthread_kill+52)
    #02 pc 00018347  /system/lib/libc.so (raise+10)
    #03 pc 00014be1  /system/lib/libc.so (__libc_android_abort+36)
    #04 pc 00012f70  /system/lib/libc.so (abort+4)
    #05 pc 00015e25  /system/lib/libc.so (__libc_fatal+16)
    #06 pc 00014c65  /system/lib/libc.so (__assert2+20)
    #07 pc 000d795c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN10dmGraphics10SetTextureEPNS_7TextureERKNS_13TextureParamsE+496)
    #08 pc 000d900c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN10dmGraphics15NewRenderTargetEPNS_7ContextEjPKNS_21TextureCreationParamsEPKNS_13TextureParamsE+612)
    #09 pc 000e683c  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so (_ZN8dmRender25RenderScript_RenderTargetEP9lua_State+1512)
    #10 pc 0010f4f8  /data/app/com.potatojam.classic.solitaire.klondike-1/lib/arm/libSolitaire_Klondike.so
Tombstone written to: /data/tombstones/tombstone_01
[ 11-26 01:49:01.121   183:  183 E/         ]
ro.product_ship = true
[ 11-26 01:49:01.121   183:  183 E/         ]
ro.debug_level = 0x4f4c


logcat-2.txt.zip (290.0 KB)

Strange, but some error with m_MaxTextureSize
We have only one texture in game:
1024x256
and second one I make dynamically in render script:

local helper = require "modules.render.helper"
local msgs = require "modules.msgs"

local RT_halfsize = 50

local function update_render_targets(self, initial)
  if not initial then
    render.delete_render_target(self.render_target)
  end
  self.RT_color_params.width, self.RT_depth_params.width = helper.win_width, helper.win_width
  self.RT_color_params.height, self.RT_depth_params.height = helper.win_height, helper.win_height

  self.render_target = render.render_target("target", {[render.BUFFER_COLOR_BIT] = self.RT_color_params,
  [render.BUFFER_DEPTH_BIT] = self.RT_depth_params})

  local x = RT_halfsize * 2 / helper.win_width
  local y = RT_halfsize * 2 / helper.win_height
  self.RT_proj = vmath.matrix4_orthographic(-RT_halfsize + x, RT_halfsize - x,
  - RT_halfsize + y, RT_halfsize - y, - 1, 1)

  self.firstFrame = true
end

local function update_window(self)
  helper.win_width = render.get_window_width()
  helper.win_height = render.get_window_height()

  if helper.win_width >= helper.win_height then
    helper.settings_width = render.get_height()
    helper.settings_height = render.get_width()
  else
    helper.settings_width = render.get_width()
    helper.settings_height = render.get_height()
  end

  helper.zoom_factor = math.min(helper.win_width / helper.settings_width, helper.win_height / helper.settings_height)
  self.projected_width = helper.win_width / helper.zoom_factor
  self.projected_height = helper.win_height / helper.zoom_factor
  helper.xoffset = - (self.projected_width - helper.settings_width) / 2
  helper.yoffset = - (self.projected_height - helper.settings_height) / 2

  helper.adjust_stretch_x = helper.win_width / helper.settings_width
  helper.adjust_stretch_y = helper.win_height / helper.settings_height
  helper.adjust_zoom = math.max(helper.adjust_stretch_x, helper.adjust_stretch_y)
  helper.adjust_fit = math.min(helper.adjust_stretch_x, helper.adjust_stretch_y)
  self.full_proj = vmath.matrix4_orthographic(helper.xoffset, helper.xoffset + self.projected_width,
  helper.yoffset, helper.yoffset + self.projected_height, - 1, 1)

  self.view = vmath.matrix4()
  self.gui_projection = vmath.matrix4_orthographic(0, helper.win_width, 0, helper.win_height, - 1, 1)
end

function init(self)
  self.tile_pred = render.predicate({"tile"})
  self.gui_pred = render.predicate({"gui"})
  self.text_pred = render.predicate({"text"})
  self.render_target_pred = render.predicate({"render_target"})

  helper.settings_width = render.get_width()
  helper.settings_height = render.get_height()

  self.clear_color = vmath.vector4(0, 0, 0, 0)
  self.clear_color.x = sys.get_config("render.clear_color_red", 0)
  self.clear_color.y = sys.get_config("render.clear_color_green", 0)
  self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
  self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)

  self.RT_color_params = {
    format = render.FORMAT_RGBA,
    width = render.get_window_width(),
    height = render.get_window_height(),
    min_filter = render.FILTER_LINEAR,
    mag_filter = render.FILTER_LINEAR,
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }
  self.RT_depth_params = {
    format = render.FORMAT_DEPTH,
    width = render.get_window_width(),
    height = render.get_window_height(),
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }

  self.clear_param = {
    [render.BUFFER_COLOR_BIT] = self.clear_color,
    [render.BUFFER_DEPTH_BIT] = 1,
    [render.BUFFER_STENCIL_BIT] = 0
  }

  self.view = vmath.matrix4()
  update_window(self)
  update_render_targets(self, true)
end

function update(self)
  render.set_depth_mask(true)
  render.clear(self.clear_param)

  render.set_viewport(0, 0, helper.win_width, helper.win_height)
  render.set_view(self.view)
  render.set_depth_mask(false)
  render.disable_state(render.STATE_DEPTH_TEST)
  render.disable_state(render.STATE_STENCIL_TEST)
  render.enable_state(render.STATE_BLEND)
  render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
  render.disable_state(render.STATE_CULL_FACE)

  render.set_projection(self.full_proj)

  if self.is_render_target then

    render.enable_render_target(self.render_target)
    if self.firstFrame then
      self.firstFrame = false
      render.clear(self.clear_param)
    end

    render.draw(self.tile_pred)
    render.disable_render_target(self.render_target)

    render.set_view(self.view)
    render.set_projection(self.RT_proj)
    render.enable_texture(0, self.render_target, render.BUFFER_COLOR_BIT)
    render.draw(self.render_target_pred)
    render.disable_texture(0, self.render_target)
  else
    render.draw(self.tile_pred)
  end

  render.set_view(self.view)
  render.set_projection(self.gui_projection)

  render.enable_state(render.STATE_STENCIL_TEST)
  render.draw(self.gui_pred)
  render.draw(self.text_pred)
  render.disable_state(render.STATE_STENCIL_TEST)

  render.set_depth_mask(false)
end

function on_message(self, message_id, message)
  if message_id == hash("clear_color") then
    self.clear_color = message.color
    self.clear_param[render.BUFFER_COLOR_BIT] = self.clear_color
  elseif message_id == hash("set_view_projection") then
    self.view = message.view
  elseif message_id == msgs.WINDOW_RESIZED then
    update_window(self)
    update_render_targets(self)
    msg.post("main:/main#main", message_id, message)
  elseif message_id == msgs.R_TARGET then
    self.is_render_target = message.isOn
    if self.is_render_target then
      update_render_targets(self)
    end
  end
end

The assert you are seeing is a check so that we don’t create a render target with dimensions greater than what is supported by the device

glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
...
assert(params.m_Height <= max_texture_size);

Maybe helper.win_width for some reason get assigned too big a value?

1 Like

I have only one place where I set this value (in render script):

 helper.win_width = render.get_window_width()
  helper.win_height = render.get_window_height()

Both device has smaller screen size than supported texture.
Is it possible that Init of render_sript runs earliar that initialization of global render module and I recieve some default value in render.get_window_width() instead of real size?
(I have no this device to check this value - logs from google console)

1 Like

There are also some glGetError checks when creating textures in dmGraphics::SetTexture which could unfortunately result in assert if they fail. However I can’t see any issues with the parameters you pass it…

But it might be a good idea to do these to things (based on my own experience) anyway:

  • Make sure your render targets are power-of-two in size (even if CLAMP_TO_EDGE should be able to handle NPOT textures).
  • Switch to NEAREST filtering unless you really need LINEAR (I guess you use the render target as a post-process so it’s always 1:1 from texture to screen fragments, so linear doesn’t really do much here anyway).
4 Likes

Cool, thanks.
I also find this:
https://aras-p.info/blog/2012/10/17/non-power-of-two-textures/

ES 2.0 has limited NPOT support (no mipmaps, no Repeat wrap mode) in core; and ES 1.1 has no NPOT support.

I try to set and retest build with next options for material:


but i have no this device(

Does somebody have somebody device with Mali-400MP2 or Mali-T830MP2 ?

UPD: :point_up_2: fixed problem on P8 Lite but not with Galaxy J1 Ace =(

UPD2: : the following render script with POT render target help me with my issue. Thank you @sven

local helper = require "modules.render.helper"
local msgs = require "modules.msgs"

local RT_halfsize = 50

local function next_pot(num)
  local result = 1
  while num > result do
    result = bit.lshift(result, 1)
  end
  return result
end

local function update_render_targets(self, initial)
  if not initial then
    render.delete_render_target(self.render_target)
  end

  local pot_width = next_pot(helper.win_width)
  local pot_height = next_pot(helper.win_height)

  self.RT_color_params.width, self.RT_depth_params.width = pot_width, pot_width
  self.RT_color_params.height, self.RT_depth_params.height = pot_height, pot_height

  self.render_target = render.render_target("target", {[render.BUFFER_COLOR_BIT] = self.RT_color_params,
  [render.BUFFER_DEPTH_BIT] = self.RT_depth_params})

  local RT_SIZE = RT_halfsize * 2
  local x = RT_SIZE / pot_width * (pot_width - helper.win_width)
  local y = RT_SIZE / pot_height * (pot_height - helper.win_height)
  local nx = RT_SIZE / helper.win_width
  local ny = RT_SIZE / helper.win_height
  self.RT_proj = vmath.matrix4_orthographic(-RT_halfsize + nx, RT_halfsize - x - nx,
  - RT_halfsize + ny, RT_halfsize - y - ny, - 1, 1)

  self.firstFrame = true
end

local function update_window(self)
  helper.win_width = render.get_window_width()
  helper.win_height = render.get_window_height()

  if helper.win_width >= helper.win_height then
    helper.settings_width = render.get_height()
    helper.settings_height = render.get_width()
  else
    helper.settings_width = render.get_width()
    helper.settings_height = render.get_height()
  end

  helper.zoom_factor = math.min(helper.win_width / helper.settings_width, helper.win_height / helper.settings_height)
  self.projected_width = helper.win_width / helper.zoom_factor
  self.projected_height = helper.win_height / helper.zoom_factor
  helper.xoffset = - (self.projected_width - helper.settings_width) / 2
  helper.yoffset = - (self.projected_height - helper.settings_height) / 2

  helper.adjust_stretch_x = helper.win_width / helper.settings_width
  helper.adjust_stretch_y = helper.win_height / helper.settings_height
  helper.adjust_zoom = math.max(helper.adjust_stretch_x, helper.adjust_stretch_y)
  helper.adjust_fit = math.min(helper.adjust_stretch_x, helper.adjust_stretch_y)
  self.full_proj = vmath.matrix4_orthographic(helper.xoffset, helper.xoffset + self.projected_width,
  helper.yoffset, helper.yoffset + self.projected_height, - 1, 1)

  self.view = vmath.matrix4()
  self.gui_projection = vmath.matrix4_orthographic(0, helper.win_width, 0, helper.win_height, - 1, 1)
end

function init(self)
  self.tile_pred = render.predicate({"tile"})
  self.gui_pred = render.predicate({"gui"})
  self.text_pred = render.predicate({"text"})
  self.render_target_pred = render.predicate({"render_target"})

  helper.settings_width = render.get_width()
  helper.settings_height = render.get_height()

  self.clear_color = vmath.vector4(0, 0, 0, 0)
  self.clear_color.x = sys.get_config("render.clear_color_red", 0)
  self.clear_color.y = sys.get_config("render.clear_color_green", 0)
  self.clear_color.z = sys.get_config("render.clear_color_blue", 0)
  self.clear_color.w = sys.get_config("render.clear_color_alpha", 0)

  self.RT_color_params = {
    format = render.FORMAT_RGBA,
    width = render.get_window_width(),
    height = render.get_window_height(),
    min_filter = render.FILTER_LINEAR,
    mag_filter = render.FILTER_LINEAR,
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }
  self.RT_depth_params = {
    format = render.FORMAT_DEPTH,
    width = render.get_window_width(),
    height = render.get_window_height(),
    u_wrap = render.WRAP_CLAMP_TO_EDGE,
    v_wrap = render.WRAP_CLAMP_TO_EDGE
  }

  self.clear_param = {
    [render.BUFFER_COLOR_BIT] = self.clear_color,
    [render.BUFFER_DEPTH_BIT] = 1,
    [render.BUFFER_STENCIL_BIT] = 0
  }

  self.view = vmath.matrix4()
  update_window(self)
  update_render_targets(self, true)
end

function update(self)
  render.set_depth_mask(true)
  render.clear(self.clear_param)

  render.set_viewport(0, 0, helper.win_width, helper.win_height)
  render.set_view(self.view)
  render.set_depth_mask(false)
  render.disable_state(render.STATE_DEPTH_TEST)
  render.disable_state(render.STATE_STENCIL_TEST)
  render.enable_state(render.STATE_BLEND)
  render.set_blend_func(render.BLEND_SRC_ALPHA, render.BLEND_ONE_MINUS_SRC_ALPHA)
  render.disable_state(render.STATE_CULL_FACE)

  render.set_projection(self.full_proj)

  if self.is_render_target then

    render.enable_render_target(self.render_target)
    if self.firstFrame then
      self.firstFrame = false
      render.clear(self.clear_param)
    end

    render.draw(self.tile_pred)
    render.disable_render_target(self.render_target)

    render.set_view(self.view)
    render.set_projection(self.RT_proj)
    render.enable_texture(0, self.render_target, render.BUFFER_COLOR_BIT)
    render.draw(self.render_target_pred)
    render.disable_texture(0, self.render_target)
  else
    render.draw(self.tile_pred)
  end

  render.set_view(self.view)
  render.set_projection(self.gui_projection)

  render.enable_state(render.STATE_STENCIL_TEST)
  render.draw(self.gui_pred)
  render.draw(self.text_pred)
  render.disable_state(render.STATE_STENCIL_TEST)

  render.set_depth_mask(false)
end

function on_message(self, message_id, message)
  if message_id == hash("clear_color") then
    self.clear_color = message.color
    self.clear_param[render.BUFFER_COLOR_BIT] = self.clear_color
  elseif message_id == hash("set_view_projection") then
    self.view = message.view
  elseif message_id == msgs.WINDOW_RESIZED then
    update_window(self)
    update_render_targets(self)
    msg.post("main:/main#main", message_id, message)
  elseif message_id == msgs.R_TARGET then
    self.is_render_target = message.isOn
    if self.is_render_target then
      update_render_targets(self)
    end
  end
end

6 Likes

We made update 1.60:

  • random deck option - now user can play with deck that can be solved 100%;
  • russian localization - we made integration of localization system into game, for now it’s just Russion and English, In future we will add more, I think.
2 Likes

How are player metrics doing?

Pretty good.
Audience is growing up stable, slow but stable (Daily app installs are bigger than daily app removes)

Retention (ios only)
Day 2: 20 - 25%
Day 7: 10-20%
Day 30: 5-10%

Now we are working on the new update with one major feature - undo, and few small improvements.
I think it will be last major update. After that only minor updates with bug fixes and so on.
Maybe in future we itegrate google play services like leaderboards, achievements etc. But for now nobody makes this NE and we have other priorities too.

UPD:
Defold analytics didn’t work for android, maybe because I removed a lot of permissions.
Retention percent actual only for IOS, we have no data for android. I’ll try to collect this data after next update.

2 Likes

Are you tracking any other data? https://www.defold.com/community/projects/86424/ Might be helpful for that

I’d be curious to know what percentage of users play on landscape vs portrait layout for example. Did you have any trouble implementing that?

Are the artifacts in the second button due to Defold’s texture compression?

No, we didn’t integrate any analytics to this project.
I’ll think about it, but I’m not sure that we have time. We do not need analytics in this project.

No, this artefacts in original image (We will fix that when finished with major features and start to distribute HTML5 version.):
google

1 Like

Have you published postmortems for any of your finished projects before?

2 Likes

No. I have a task in my personal task manager to make postmortem for Bring me cakes, but I have no time, and we still working on the game :slight_smile:

8 Likes

We made update 1.71:

  • Implemented UNDO button!
  • Added sound settings.
  • Minor bugfixes and improvements.

We will back to the project in future for adding of Google Play Services and Game Center but for now project is frozen.
We have plans to use this solitaire “engine” for make more original game in future.

9 Likes