function direction = myAI_JRM(board) %DESCRIPTION: Decides which move to make next given a 2048 board. %AUTHOR: Jeremy R. Manning (manning3@princeton.edu) %DATE: April 17, 2014 %the list of moves to choose from moves = {'left','up','right','down'}; %N_RUNS specifies how many times to simulate each move %DEPTH specifies how many moves to look ahead %relative_weights specifies how much to factor in each criteria (used to %evaluate how "good" a board position is): % first element: overall score % second element: number of empty spaces % third element: a gradient that pushes high numbers to the bottom right % fourth element: a score that gives better values to boards with more % similar numbers (e.g. a board with all of the same % number would get the highest score). %EXPONENT: controls how quickly the gradient (whose influence is controlled %by the third element of relative weights vector) falls off. Setting %EXPONENT = 0 nullifies the effect of the gradient (i.e. no position %preference for the tiles). %this section re-weigths the relative preferences and the number of %positions to look ahead depending on which tiles are on the board. in %general, early moves are done relatively more quickly than later moves. N_RUNS = 1; if sum(board(:) == 1024) == 2 DEPTH = 4; relative_weights = [100 25 0.1 0.1]; elseif max(board(:)) >= 512 DEPTH = 3; relative_weights = [50 50 20 20]; elseif max(board(:)) >= 128 DEPTH = 2; relative_weights = [50 75 40 10]; else DEPTH = 1; relative_weights = [25 100 80 5]; end EXPONENT = 1; %compute all possible moves up to specified depth move_combos = get_move_combos(moves, DEPTH); %compute gradient (that pushes high-valued tiles to the bottom right) board_weights = get_board_weights(board, EXPONENT); %initialize the scores (need one score for each category, for each %combination of moves) [scores, empties, weights, evenness] = deal(zeros(1, size(move_combos, 1))); %estimate the scores for each combination of moves, averaged over N_RUNS %trials for i = 1:size(move_combos, 1) for j = 1:N_RUNS [s, e, w, v] = estimate_improvement(board, move_combos(i, :), board_weights); scores(i) = scores(i) + s; empties(i) = empties(i) + e; weights(i) = weights(i) + w; evenness(i) = evenness(i) + v; end scores(i) = scores(i)/N_RUNS; empties(i) = empties(i)/N_RUNS; weights(i) = weights(i)/N_RUNS; evenness(i) = evenness(i)/N_RUNS; end %compute the optimal move combination given the scores and the relative %weights ind = opt([scores ; empties ; weights ; evenness], relative_weights); best_combo = move_combos(ind, :); %return the first move in the combination direction = best_combo{1}; %if the move didn't change the board, move randomly until the board does %change. while board_unchanged(board, direction) direction = moves{select_random(1:length(moves))}; end %%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% HELPER FUNCTIONS %%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% function[x] = board_unchanged(board, direction) %does this move have any effect? tmp = TwentyFortyEight(false); tmp.Board = board; tmp.move(direction); x = all(nanequal(board(:), tmp.Board(:))); function[x] = nanequal(a, b) %computes element-wise equality, taking nan's into account x = (isnan(a) & isnan(b)) | (a == b); function[score, empty, weighted_position, evenness] = estimate_improvement(board, ms, w) %for the given combination of moves (ms), estimate the scores of the result tmp = TwentyFortyEight(false); tmp.Board = board; for i = 1:length(ms) tmp.move(ms{i}); end score = nan_max(tmp.Scores); empty = sum(isnan(tmp.Board(:))); weighted_position = nansum(tmp.Board(:).*w(:)); counts = arrayfun(@(i)(count(tmp.Board, i)), 2.^(1:11)); evenness = mean(counts(counts ~= 0)); function[x] = nan_max(x) %computes the global maximum of a matrix, ignoring nan's x = max(x(~isnan(x(:)))); function[x] = select_random(xs) %selects a random element of the vector xs x = xs(randi(length(xs))); function[ms] = get_move_combos(moves, depth) %compute every combination of moves, up to the specified depth if depth == 1 ms = moves(:); else xs = fullfact(length(moves).*ones(1, depth)); ms = moves(xs); end function[w] = get_board_weights(board, exponent) %compute the gradient that pushes high-valued tiles to the lower right d = size(board, 1); w = repmat(1:d, d, 1).*repmat(transpose(1:4), 1, d); w = w.^exponent; function[ind] = opt(x, weights) %assign a value to each move combination by weighting the given %combination-specific scores weights = weights./sum(weights); ind = zeros(1, size(x, 2)); for i = 1:size(x, 1) ind = ind + weights(i).*normalize(x(i, :)); end ind = find((ind == max(ind))); if length(ind) > 1 ind = select_random(ind); end function[x] = normalize(x) %normalize the values in x (ignoring nan's) to range from 0 to 1. then set %all nan's to 0. x = x - nanmin(x(:)); x = x./nanmax(x(:)); x(isnan(x)) = 0;]]>

Nice way to hide your email adress from spambots :)

encrypt_email = @(email) [double(email(1)) diff(double(email))]

]]>I improved a bit my previous code. Now, while keeping the main strategy, I look 3 steps ahead. This leads to a significant improvement. I only post the modified code segment now.

After 1000 simulations: Highest score: 22312 128 0.4% 256 6.2% 512 39.2% 1024 46.5% 2048 7.7%

... % look 3 steps into the left branch [b1, benefits(1), ~] = executeStep(board, 'left'); [b11, benefitsL(1), ~] = executeStep(b1, 'left'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(1) = benefitsL(1) + max(benefitsL2); [b11, benefitsL(2), ~] = executeStep(b1, 'right'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(2) = benefitsL(2) + max(benefitsL2); [b11, benefitsL(3), ~] = executeStep(b1, 'down'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(3) = benefitsL(3) + max(benefitsL2); benefits2(1) = benefits(1) + max(benefitsL); % look 3 steps into the right branch [b1, benefits(2), ~] = executeStep(board, 'right'); [b11, benefitsL(1), ~] = executeStep(b1, 'left'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(1) = benefitsL(1) + max(benefitsL2); [b11, benefitsL(2), ~] = executeStep(b1, 'right'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(2) = benefitsL(2) + max(benefitsL2); [b11, benefitsL(3), ~] = executeStep(b1, 'down'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(3) = benefitsL(3) + max(benefitsL2); benefits2(2) = benefits(2) + max(benefitsL); % look 3 steps into the down branch [b1, benefits(3), ~] = executeStep(board, 'down'); [b11, benefitsL(1), ~] = executeStep(b1, 'left'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(1) = benefitsL(1) + max(benefitsL2); [b11, benefitsL(2), ~] = executeStep(b1, 'right'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(2) = benefitsL(2) + max(benefitsL2); [b11, benefitsL(3), ~] = executeStep(b1, 'down'); [~, benefitsL2(1), ~] = executeStep(b11, 'left'); [~, benefitsL2(2), ~] = executeStep(b11, 'right'); [~, benefitsL2(3), ~] = executeStep(b11, 'down'); benefitsL(3) = benefitsL(3) + max(benefitsL2); benefits2(3) = benefits(3) + max(benefitsL); % decide to move to overall max profit [maxBenefit, index] = max(benefits2); ...]]>

My strategy is to focus on filling up the last row, trying to keep the largest number in the bottom-left corner by avoiding “right” steps when the bottom row is not filled with numbers. I oly allow “up” moves when there is no other way to go.

I implemented a function that executes steps so I can look 2 level deep into the game and the move that I choose is the one that results in the maximum profit (score gain) in the upcoming 2 moves.

The result look quite well, with a few games WON! :) Highest score: 22264 128 0.5% 256 7% 512 45% 1024 43.4% 2048 4.1%

function direction = myAI(board) up = 'up'; down = 'down'; left = 'left'; right = 'right'; if (isnan(board(4,1)) && isLeftPossible(board)) direction = left; return; end if (isnan(sum(board(4,:))) && isDownPossible(board)) direction = down; return; end [b1, benefits(1), s(1)] = executeStep(board, 'left'); [~, benefitsL(1), stepsL(1)] = executeStep(b1, 'left'); [~, benefitsL(2), stepsL(2)] = executeStep(b1, 'right'); [~, benefitsL(3), stepsL(3)] = executeStep(b1, 'down'); benefits2(1) = benefits(1) + max(benefitsL); [b2, benefits(2), s(2)] = executeStep(board, 'right'); [~, benefitsL(1), stepsL(1)] = executeStep(b2, 'left'); [~, benefitsL(2), stepsL(2)] = executeStep(b2, 'right'); [~, benefitsL(3), stepsL(3)] = executeStep(b2, 'down'); benefits2(2) = benefits(2) + max(benefitsL); [b3, benefits(3), s(3)] = executeStep(board, 'down'); [~, benefitsL(1), stepsL(1)] = executeStep(b3, 'left'); [~, benefitsL(2), stepsL(2)] = executeStep(b3, 'right'); [~, benefitsL(3), stepsL(3)] = executeStep(b3, 'down'); benefits2(3) = benefits(3) + max(benefitsL); [maxBenefit, index] = max(benefits2); if (index == 1 && maxBenefit > 0 && isLeftPossible(board)) direction = left; return; elseif (benefits2(2) > benefits2(3) && ~isnan(sum(board(4,:))) && isRightPossible(board)) direction = right; return; elseif (benefits2(3) > benefits2(2) && isDownPossible(board)) direction = down; return; end if (index == 2 && maxBenefit > 0 && ~isnan(sum(board(4,:))) && isRightPossible(board)) direction = right; return; elseif (benefits2(1) > benefits2(3) && isLeftPossible(board)) direction = left; return; elseif (benefits2(3) > benefits2(1) && isDownPossible(board)) direction = down; return; end if (index == 3 && maxBenefit > 0 && isDownPossible(board)) direction = down; return; elseif (benefits2(2) > benefits2(1) && ~isnan(sum(board(4,:))) && isRightPossible(board)) direction = right; return; elseif (benefits2(1) > benefits2(2) && isLeftPossible(board)) direction = left; return; end if (isDownPossible(board)) direction = down; return; end if (isLeftPossible(board)) direction = left; return; end if (isRightPossible(board)) direction = right; return; end direction = up; end function [resBoard profit numMerges] = executeStep(board, step) b = board; restirct = zeros(4,4); profit = 0; numMerges = 0; index = 0; if (strcmp(step, 'left')) for i=1:4 for j=2:4 if (~isnan(b(i,j))) if (~isnan(b(i,j-1)) && b(i,j) ~= b(i,j-1)) continue; end for n=j-1:-1:1 index = 0; if(~isnan(b(i,n))) index = n; break; end end if (index == 0) b(i,1) = b(i,j); b(i,j) = NaN; elseif (b(i,j) == b(i,index) && restirct(i,index) == 0) b(i,index) = b(i,index)*2; restirct(i,index) = 1; b(i,j) = NaN; profit = profit + b(i,index); numMerges = numMerges + 1; elseif (b(i,j) == b(i,index) && restirct(i,index) == 1) b(i,index+1) = b(i,j); b(i,j) = NaN; elseif (b(i,j) ~= b(i,index)) b(i,index+1) = b(i,j); b(i,j) = NaN; else b(i,index) = b(i,j); b(i,j) = NaN; end; end end end elseif (strcmp(step, 'right')) for i=1:4 for j=3:-1:1 if (~isnan(b(i,j))) if (~isnan(b(i,j+1)) && b(i,j) ~= b(i,j+1)) continue; end for n=j+1:4 index = 0; if(~isnan(b(i,n))) index = n; break; end end if (index == 0) b(i,4) = b(i,j); b(i,j) = NaN; elseif (b(i,j) == b(i,index) && restirct(i,index) == 0) b(i,index) = b(i,index)*2; restirct(i,index) = 1; b(i,j) = NaN; profit = profit + b(i,index); numMerges = numMerges + 1; elseif (b(i,j) == b(i,index) && restirct(i,index) == 1) b(i,index-1) = b(i,j); b(i,j) = NaN; elseif (b(i,j) ~= b(i,index)) b(i,index-1) = b(i,j); b(i,j) = NaN; else b(i,index) = b(i,j); b(i,j) = NaN; end; end end end elseif (strcmp(step, 'down')) for i=3:-1:1 for j=1:4 if (~isnan(b(i,j))) if (~isnan(b(i+1,j)) && b(i,j) ~= b(i+1,j)) continue; end for n=i+1:4 index = 0; if(~isnan(b(n,j))) index = n; break; end end if (index == 0) b(4,j) = b(i,j); b(i,j) = NaN; elseif (b(i,j) == b(index,j) && restirct(index,j) == 0) b(index,j) = b(index,j)*2; restirct(index,j) = 1; b(i,j) = NaN; profit = profit + b(index,j); numMerges = numMerges + 1; elseif (b(i,j) == b(index,j) && restirct(index,j) == 1) b(index-1,j) = b(i,j); b(i,j) = NaN; elseif (b(i,j) ~= b(index,j)) b(index-1,j) = b(i,j); b(i,j) = NaN; else b(index,j) = b(i,j); b(i,j) = NaN; end; end end end elseif (strcmp(step, 'up')) for i=2:4 for j=1:4 if (~isnan(b(i,j))) if (~isnan(b(i-1,j)) && b(i,j) ~= b(i-1,j)) continue; end for n=i-1:-1:1 index = 0; if(~isnan(b(n,j))) index = n; break; end end if (index == 0) b(1,j) = b(i,j); b(i,j) = NaN; elseif (b(i,j) == b(index,j) && restirct(index,j) == 0) b(index,j) = b(index,j)*2; restirct(index,j) = 1; b(i,j) = NaN; profit = profit + b(index,j); numMerges = numMerges + 1; elseif (b(i,j) == b(index,j) && restirct(index,j) == 1) b(index+1,j) = b(i,j); b(i,j) = NaN; elseif (b(i,j) ~= b(index,j)) b(index+1,j) = b(i,j); b(i,j) = NaN; else b(index,j) = b(i,j); b(i,j) = NaN; end; end end end end resBoard = b; end function res = isLeftPossible(board) res = false; for i=1:4 for j=2:4 if (~isnan(board(i,j))) % if left number simmilar or ther is void on left if (board(i,j) == board(i,j-1) || isnan(board(i,j-1))) res = true; return; end end end end end function res = isRightPossible(board) res = false; for i=1:4 for j=1:3 if (~isnan(board(i,j))) % if left number simmilar or ther is void on left if (board(i,j) == board(i,j+1) || isnan(board(i,j+1))) res = true; return; end end end end end function res = isDownPossible(board) res = false; for i=1:3 for j=1:4 if (~isnan(board(i,j))) % if left number simmilar or ther is void on left if (board(i,j) == board(i+1,j) || isnan(board(i+1,j))) res = true; return; end end end end end]]>

function direction = downAI(board) % algorithm that tries to put biggest numbers in lower right corner % freeware dirs = {'down', 'right', 'left', 'up'}; persistent prevdir oldboard if isempty(prevdir) prevdir = 1; end if isempty(oldboard) oldboard = nan(4,4); end if isequaln(board,oldboard) dir = prevdir + 1; else dir = 1; end direction = dirs{dir}; prevdir = dir; oldboard = board; end

A thousand iterations yields the following scores

Highest score: 7552 32 0.6% 64 8.9% 128 37.7% 256 48.4% 512 4.4%]]>

http://stackoverflow.com/questions/22342854/what-is-the-optimal-algorithm-for-the-game-2048

My algorithm is similar to the expctimax algorithm of nneonneo, though they are looking 8 moves ahead and have a different cost function. ]]>

I have run this code on one game only so far. It mostly runs as fast as a human could play (just pressing random keys), but does slow down when you get to only 2 free tiles, as I look 5 moves ahead at that point. However, it’s only about a second in that case too (on my decent PC). The result was: score – 36560, highest square – 2048, moves – 1900.

I’m sure improvements could be made, especially in taking into account the probability of the game ending, and minimizing this.

Here’s the code:

function d = oliverAI(B) % Convert to log board B(isnan(B)) = 1; B = uint16(log2(B)); % How many moves should we look ahead movesAhead = 3 + sum(sum(B(:) == 0) < [3 7]); % Slide in each direction d = []; s = []; A = zeros(4, 4, 0, 'uint16'); for d_ = 1:4 [C, s_] = slide(B, d_); if ~isequal(B, C) A = cat(3, A, C); d = [d d_]; s = [s; s_]; end end B = A; p = ones(numel(d), 1); for a = 2:movesAhead % For each board in B, consider all possible positions and values for % the new tile B = reshape(B, 16, []); N = sum(B == 0, 1); % Number of free squares I = zeros(sum(N)*2, 1); i = 0; % Expand the boards array to hold all possible examples for b = find(N) I(i+1:i+N(b)*2) = b; i = i + N(b) * 2; end B = B(:,I); d = d(:,I); p = p(I); s = s(I); i = 0; % For each board for b = find(N) j = find(B(:,i+1) == 0); % Add in the new 2s for c = 1:N(b) p(c+i) = p(c+i) * (0.9 / N(b)); B(j(c),c+i) = 1; end i = i + N(b); % Add in the new 4s for c = 1:N(b) p(c+i) = p(c+i) * (0.1 / N(b)); B(j(c),c+i) = 2; end i = i + N(b); end B = reshape(B, 4, 4, []); % Slide all the boards in each direction A = zeros(4, 4, 0, 'uint16'); d__ = zeros(a, 0); p_ = []; s_ = []; for d_ = 1:4 [C, s__] = slide(B, d_); M = any(reshape(B ~= C, 16, [])); A = cat(3, A, C(:,:,M)); p_ = [p_; p(M)]; s_ = [s_; s(M)+s__(M)]; d__ = [d__ [d(:,M); zeros(1, sum(M))+d_]]; end B = A; p = p_; s = s_; d = d__; end % Compute the expected gain in score for a each set of moves and maximize % over this [s_, d_] = max(accumarray(((4 .^ (0:movesAhead-1)) * (d-1))'+1, double(s(:)) .* p(:), [4^movesAhead 1])); d_ = mod(d_ - 1, 4) + 1; % Maximize the expected number of zeros after the moves %[d, d] = max(accumarray(d(1,:)', sum(reshape(B, 16, []) == 0, 1)' .* p(:), [4 1])); % Output the string for the direction d = {'up', 'down', 'right', 'left'}; d = d{d_}; function [B, s] = slide(B, dir) persistent lut lutf luts if isempty(lut) % Precompute the fast lookup for sliding up [a, b, c, d] = ndgrid(0:13); a = [a(:)'; b(:)'; c(:)'; d(:)']; lut = zeros(4, size(a, 2), 'uint16'); % Compress for better use of cache luts = zeros(size(a, 2), 1, 'uint16'); % Compress for better use of cache s_ = 2 .^ (1:14); % Compute the slide manually for b = 1:size(a, 2) d = a(:,b); d = d(d ~= 0); s = 0; for c = 1:numel(d)-1 if d(c) == d(c+1) && d(c) d(c) = d(c) + 1; s = s + s_(d(c)); d(c+1:end) = [d(c+2:end); 0]; end end lut(1:numel(d),b) = d; luts(b) = s; end lutf = flipud(lut); % Make a flipped table too end % Do the slide using fast table look up switch dir case 1 I = sum(bsxfun(@times, uint16([1 14 196 2744]'), B), 1, 'native') + uint16(1); B = reshape(lut(:,I), 4, 4, []); case 2 I = sum(bsxfun(@times, uint16([2744 196 14 1]'), B), 1, 'native') + uint16(1); B = reshape(lutf(:,I), 4, 4, []); case 3 I = sum(bsxfun(@times, uint16([2744 196 14 1]), B), 2, 'native') + uint16(1); B = permute(reshape(lutf(:,I), 4, 4, []), [2 1 3]); case 4 I = sum(bsxfun(@times, uint16([1 14 196 2744]), B), 2, 'native') + uint16(1); B = permute(reshape(lut(:,I), 4, 4, []), [2 1 3]); end if nargout > 1 s = sum(reshape(luts(I), 4, []), 1, 'native')'; end]]>

@Guanfeng Liang, at least for this post, I don’t want to change the original class definition, in order to keep everything the same and fair for everyone. So I simply replaced your

~tryGame.isMoved

with

isequalwithequalnans(tryGame.Board, board)

These should be equivalent. You also had

score = tryGame.thisScore;

which I replaced with

score = max(tryGame.Scores);

Keep them coming!

]]>