πŸŽͺ νΌλ ˆμ΄λ“œ AI

νΌλ ˆμ΄λ“œ AIλŠ” νΌλ ˆμ΄λ“œ κ²Œμž„μ— μ‚¬λžŒ ν”Œλ ˆμ΄μ–΄ 외에 AI 봇을 μΆ”κ°€ν•˜λŠ” κΈ°λŠ₯을 κ°œλ°œν•˜κΈ° μœ„ν•œ ν”„λ‘œμ νŠΈμ΄λ©°, 이 νŽ˜μ΄μ§€λŠ” κ°„λ‹¨ν•œ μ„€λͺ…κ³Ό νŠœν† λ¦¬μ–Όμ„ ν¬ν•¨ν•©λ‹ˆλ‹€. νΌλ ˆμ΄λ“œ κ²Œμž„μ„ ν”Œλ ˆμ΄ν•˜λŠ” μ—¬λŸ¬κ°€μ§€ κ°€λŠ₯ν•œ μ•Œκ³ λ¦¬μ¦˜μ„ μž‘μ„±ν•΄λ³΄κ³ , κ²½μŸμ— μ°Έμ—¬ν•΄ κ°€μž₯ 높은 점수λ₯Ό νšλ“ν•˜μ„Έμš”!

πŸ“Œ μ‹œμž‘ν•˜κΈ°μ— μ•žμ„œ

AI 봇 κ°œλ°œμ— μ‚¬μš©λ˜λŠ” ν”„λ‘œκ·Έλž˜λ° μ–Έμ–΄λŠ” JavaScriptμž…λ‹ˆλ‹€. Python을 μ‚¬μš©ν•˜λ©΄ μ•ˆλ˜λŠ”κ±ΈκΉŒ? ν•˜λŠ” 생각이 λ“ λ‹€λ©΄, λ‹€μŒ 인용ꡬλ₯Ό λ– μ˜¬λ € λ΄…μ‹œλ‹€.

"Any application that can be written in JavaScript, will eventually be written in JavaScript."

당신은 ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

⛳️ μ‹œμž‘ν•˜κΈ°

νΌλ ˆμ΄λ“œλ₯Ό ν”Œλ ˆμ΄ν•˜λŠ” 봇은 단 두 개의 ν•¨μˆ˜λ‘œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ‚˜μ˜ 차둀에 ν•Έλ“œμ—μ„œ 무슨 μΉ΄λ“œλ₯Ό 낼지 κ²°μ •ν•˜λŠ” ν•¨μˆ˜ play_card()와, λ§ˆμ§€λ§‰ λΌμš΄λ“œκ°€ λλ‚˜κ³  ν•Έλ“œμ—μ„œ 무슨 μΉ΄λ“œ 두 μž₯을 내렀놓을지 κ²°μ •ν•˜λŠ” ν•¨μˆ˜ play_hidden()μž…λ‹ˆλ‹€.

  • play_card()의 결과값은 { 0, 1, 2, 3, 4 } μ€‘μ˜ ν•˜λ‚˜μž…λ‹ˆλ‹€.
  • play_hidden()의 결과값은 { [0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3] } μ€‘μ˜ ν•˜λ‚˜μž…λ‹ˆλ‹€ (λ°°μ—΄λ‚΄ μˆœμ„œλŠ” λ¬΄κ΄€ν•©λ‹ˆλ‹€).

예λ₯Ό λ“€μ–΄, μ–Έμ œλ‚˜ λ¬΄μž‘μœ„λ‘œ ν”Œλ ˆμ΄ν•˜λŠ” λͺ¨μžμž₯수의 μ•Œκ³ λ¦¬μ¦˜μ€ λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

function play_card() {
  // Math.random()은 [0, 1) κ΅¬κ°„μ˜ μ‹€μˆ˜λ₯Ό λ°˜ν™˜ν•¨
  return Math.floor(Math.random() * 5);
}

function play_hidden() {
  // [0, 1, 2, 3]을 μ…”ν”Œν•˜μ—¬ μ•žμ˜ 두 μ›μ†Œλ§Œ 취함
  return [0, 1, 2, 3].sort(() => Math.random() - .5).slice(0, 2);
}

πŸ”­ κ΄€μΈ‘ κ°€λŠ₯ν•œ 정보 μ΄μš©ν•˜κΈ°

λ¬΄μž‘μœ„λ‘œ ν”Œλ ˆμ΄ν•˜λŠ” 것보닀 μž˜ν•˜κ³  μ‹Άλ‹€λ©΄, 주어진 정보λ₯Ό ν™œμš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. play_card()와 play_hidden() ν•¨μˆ˜μ—λŠ” 사싀 첫번째이자 μœ μΌν•œ 인자(state)둜 ν”Œλ ˆμ΄μ–΄ μž…μž₯μ—μ„œ ν˜„μž¬ κ²Œμž„μ—μ„œ κ΄€μΈ‘ κ°€λŠ₯ν•œ 정보가 μ£Όμ–΄μ§‘λ‹ˆλ‹€.

이 인자(state)μ—λŠ” λ‹€μŒκ³Ό 같은 정보가 λ“€μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

{
  num_players: 3,                            // ν”Œλ ˆμ΄μ–΄ 수
  stack_size: 43,                           // 남은 λ“œλ‘œμš° 수
  parade: [37, 61, 9, 4, 3, 27],            // νΌλ ˆμ΄λ“œμ— 놓인 μΉ΄λ“œ
  scores: [[], [45, 55], []],               // μ§€κΈˆκΉŒμ§€ νšλ“ν•œ 점수
  hand: [5, 8, 22, 44, 64],                 // λ‚˜μ˜ μ†νŒ¨
  history: [                                // μ§€κΈˆκΉŒμ§€ ν”Œλ ˆμ΄ 기둝
    {
      player: 1,                                // ν”Œλ ˆμ΄μ–΄ 번호
      card: 3,                                  // μ„ νƒν•œ μΉ΄λ“œ
      scores: [45, 55],                         // ν•΄λ‹Ή 턴에 얻은 점수
    },
    {
      player: 2,
      card: 27,
      scores: [],                               // ν•΄λ‹Ή 턴에 점수 얻지 μ•ŠμŒ     
    },
  ],
  is_last_round: false,                     // λ§ˆμ§€λ§‰ λΌμš΄λ“œ
}

μœ„μ˜ μ˜ˆμ‹œλŠ” κ²Œμž„μ„ μ‹œμž‘ν•˜κ³  μ•žμ„œ λ‹€λ₯Έ μ‚¬λžŒλ“€(player: 1, player: 2)이 ν•œλ²ˆμ”©, 총 두턴 ν”Œλ ˆμ΄ ν›„ λ‚΄κ°€ ν”Œλ ˆμ΄ν•  차둀인 μƒν™©μž…λ‹ˆλ‹€. ν”Œλ ˆμ΄μ–΄μ˜ ꡬ뢄은 λ‚˜λ‘œλΆ€ν„° 0으둜 μ‹œμž‘ν•΄ μ‹œκ³„ λ°©ν–₯으둜(= μ΄μ–΄μ„œ ν”Œλ ˆμ΄ν•˜λŠ” μ°¨λ‘€λŒ€λ‘œ) 1, 2, ..., (num_players - 1)이 λ©λ‹ˆλ‹€. κ²Œμž„μ˜ 첫 ν„΄ ν”Œλ ˆμ΄μ–΄μ— 관계 없이 λ‚΄κ°€ player: 0이며, 전체 ν”Œλ ˆμ΄μ–΄κ°€ μ§€κΈˆκΉŒμ§€ νšλ“ν•œ 점수λ₯Ό λ‚˜νƒ€λ‚΄λŠ” scoresμ—μ„œλ„ 0번째 μ›μ†ŒμΈ λ°°μ—΄(scores[0])이 λ‚˜μ˜ 점수λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€.

parade, scores, hand, history.card, history.scores 등에 λ“±μž₯ν•˜λŠ” μˆ«μžλŠ” 0μ—μ„œ 65 μ‚¬μ΄λ‘œ, 각 μˆ«μžλŠ” μΉ΄λ“œ ν•œ μž₯의 색깔과 크기λ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€. 숫자λ₯Ό 11둜 λ‚˜λˆˆ λͺ«μ΄ 색깔을 λ‚˜νƒ€λ‚΄κ³  11둜 λ‚˜λˆˆ λ‚˜λ¨Έμ§€κ°€ 크기λ₯Ό λ‚˜νƒ€λƒ…λ‹ˆλ‹€. 색깔은 숫자λ₯Ό 11둜 λ‚˜λˆˆ λͺ«μ— 따라 각각 0:νŒŒλž‘, 1:초둝, 2:λ…Έλž‘, 3:λΉ¨κ°•, 4:보라, 5:κ²€μ •μž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, scores[1] = [45, 55]λŠ” player: 1의 점수 보라색 1κ³Ό 검정색 0 μΉ΄λ“œλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€.

parade와 hand의 정보λ₯Ό μ‘°ν•©ν•˜λ©΄ μ†νŒ¨μ˜ { 0, 1, 2, 3, 4 } 쀑 μ–΄λ–€ μΉ΄λ“œλ₯Ό λ‚΄λŠ” 것이 μœ λ¦¬ν• μ§€ 계산할 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, scoresλ₯Ό 톡해 κ²Œμž„μ˜ νŒμ„Έμ™€ ν–₯방을 κ°€λŠ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

🏹 상황에 따라 λ‹€λ₯Έ μ „λž΅ μ‚¬μš©ν•˜κΈ°

μƒμˆ ν•œ 것과 같이, 인자의 정보λ₯Ό μ΄μš©ν•΄ 세뢀적인 μ „λž΅μ„ ꡬ상할 수 μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜λŠ” 이해λ₯Ό 돕기 μœ„ν•œ JavaScript ꡬ문의 μ˜ˆμ‹œμž…λ‹ˆλ‹€.

1. λ‚΄ μ μˆ˜μ—μ„œ νŠΉμ • 색깔(λ…Έλž‘)의 μΉ΄λ“œ 개수
// κΈ΄ 버전
var color = 2,
    counts = 0;

for (var i = 0; i < state.scores[0].length; i += 1) {
  // parseIntλŠ” μ‹€μˆ˜μ˜ μ •μˆ˜λΆ€λ§Œμ„ 취함
  if (parseInt(state.scores[0][i] / 11) === color) {
    counts += 1;
  }
}
// 짧은 버전
var counts = state.scores[0].filter(e => parseInt(e / 11) === 2).length;
2. λ‚΄ μ†νŒ¨μ— μžˆλŠ” μΉ΄λ“œμ˜ 크기의 μ΅œμ†Œκ°’
// κΈ΄ 버전
var minimum = Infinity;     // Infinity λŒ€μ‹  μΆ©λΆ„νžˆ 큰 μž„μ˜μ˜ 수λ₯Ό μ‚¬μš©ν•΄λ„ 됨

for (var i = 0; i < state.hand.length; i += 1) {
  var size = state.hand[i] % 11;
  if (size < minimum) {
    minimum = state.hand[i] % 11;
  }
}
// 짧은 버전
var minimum = Math.min.apply(null, state.hand.map(e => e % 11));
// λ˜λ‹€λ₯Έ 짧은 버전
var minimum = state.hand.map(e => e % 11).reduce((a, b) => Math.min(a, b), Infinity);
3. νŠΉμ • μΉ΄λ“œ(빨강색 9)λ₯Ό λ‚Ό λ•Œ νΌλ ˆμ΄λ“œμ—μ„œ κ°€μ Έμ˜€λŠ” μΉ΄λ“œ
// κΈ΄ 버전
var card = 42,
    expected_scores = [];

var color = parseInt(card / 11),
    size = card % 11;

for (var i = 0; i < state.parade.length - size; i += 1) {
  if (parseInt(state.parade[i] / 11) === color || state.parade[i] % 11 <= size) {
    expected_scores.push(state.parade[i]);
  }
}
// 짧은 버전
var card = 42;
var expected_scores = state.parade
  .filter(e => state.parade.indexOf(e) < state.parade.length - (card % 11))         // λ§ˆμŠ€ν‚Ή
  .filter(e => parseInt(e / 11) === parseInt(card / 11) || e % 11 <= card % 11);    // λ›°μ³λ‚˜κ°„ μΉ΄λ“œ
4. μƒ‰κΉ”λ³„λ‘œ ν˜„μž¬ λ…μ ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ μΉ΄λ“œ 개수
var threshold = [];

for (var i = 0; i < 6; i += 1) {
  var counts = state.scores.map(e => e.filter(e => parseInt(e / 11) === i)).map(e => e.length),
      minimum = counts.reduce((a, b) => Math.min(a, b), 11),
      maximum = counts.reduce((a, b) => Math.max(a, b), 0);

  threshold.push(state.num_players === 2 ? minimum + 2 : maximum);     // 2인일 경우 μ΅œμ†Œκ°’+2둜, κ·Έμ™Έ μ΅œλŒ€κ°’μœΌλ‘œ
}
5. μƒ‰κΉ”λ³„λ‘œ μ§€κΈˆκΉŒμ§€ μœ„μΉ˜κ°€ ν™•μΈλœ μΉ΄λ“œ 개수
// κΈ΄ 버전
var disclosed = Array(6).fill(0);

for (var i = 0; i < state.parade.length; i += 1) {
  // νΌλ ˆμ΄λ“œ
  var color = parseInt(state.parade[i] / 11);
  disclosed[color] += 1;
}

for (var i = 0; i < state.num_players; i += 1) {
  // 각 ν”Œλ ˆμ΄μ–΄μ˜ 점수 μΉ΄λ“œ
  for (var j = 0; j < state.scores[i].length; j += 1) {
    var color = parseInt(state.scores[i][j] / 11);
    disclosed[color] += 1;
  }
}

for (var i = 0; i < state.hand.length; i += 1) {
  // λ‚΄ μ†νŒ¨
  var color = parseInt(state.scores[i] / 11);
  disclosed[color] += 1;
}
// 짧은 버전
var disclosed = Array(6).fill(0).map((e, i) => state.parade
  .concat(state.scores.reduce((a, b) => a.concat(b), []))
  .concat(state.hand)
  .filter(e => parseInt(e / 11) === i)
  .length)

Array λ©”μ†Œλ“œμ˜ 인자둜 쓰인 ν™”μ‚΄ν‘œ ν•¨μˆ˜μ˜ ν‘œκΈ°λ²•μ— λŒ€ν•΄μ„œλŠ” λ‹€μŒ 링크 의 μ„€λͺ…을 μ°Έκ³ ν•΄μ£Όμ„Έμš”.

이제 μ‹€μ œ state 인자λ₯Ό μ‚¬μš©ν•˜λ„λ‘ μž‘μ„±λœ AI 봇을 μ‚΄νŽ΄λ΄…μ‹œλ‹€. λ‹€μŒμ€ 항상 이번 턴에 νšλ“ν•˜λŠ” 점수 μΉ΄λ“œμ˜ 합을 μ΅œμ†Œν™”ν•˜λ„λ‘ ν”Œλ ˆμ΄ν•˜λŠ” μ—¬μ™•μ˜ μ•Œκ³ λ¦¬μ¦˜μž…λ‹ˆλ‹€. 주어진 턴에 νΌλ ˆμ΄λ“œμ— κΉ”λ¦° μΉ΄λ“œμ™€ μ†νŒ¨λ₯Ό 비ꡐ해 μ΅œμ†Œμ˜ μ μˆ˜λ§Œμ„ κ°€μ Έμ˜€λ € ν•˜κ³ , λ§ˆμ§€λ§‰μ— 두 μž₯ 내렀놓을 μΉ΄λ“œλ₯Ό κ³ λ₯Ό λ•Œλ„ 크기가 μž‘μ€ μΉ΄λ“œλ₯Ό 골라 λ‚΄λ €λ†“μŠ΅λ‹ˆλ‹€. λŒ€μ²΄λ‘œ 점수λ₯Ό 적게 μ–»κ² μ§€λ§Œ, 색깔 독점 κ·œμΉ™μ„ 포함해도 ν”Œλ ˆμ΄λ₯Ό 잘 ν• μ§€λŠ” 두고 봐야 ν•©λ‹ˆλ‹€. 그리고 맀턴 λˆˆμ•žμ˜ μ΄μ΅λ§Œμ„ μΆ”κ΅¬ν•˜λŠ” 것 λ˜ν•œ μ—¬μ™•μ˜ μ•½μ μž…λ‹ˆλ‹€.

function play_card(state) {
  var adds_numbers_sum = state.hand.map(card =>
    state.parade
      .filter(e => state.parade.indexOf(e) < state.parade.length - (card % 11))         // λ§ˆμŠ€ν‚Ή
      .filter(e => parseInt(e / 11) === parseInt(card / 11) || e % 11 <= card % 11)     // λ›°μ³λ‚˜κ°„ μΉ΄λ“œ
      .map(e => e % 11)                                                                 // 의 크기
      .reduce((a, b) => a + b, 0));                                                     // 의 ν•©

  var min = Math.min.apply(null, adds_numbers_sum),
      argmin = adds_numbers_sum.indexOf(min);

  return argmin;
}

function play_hidden(state) {
  var numbers = state.hand.map(e => e % 11 + Math.random());        // λ…Έμ΄μ¦ˆ μ•ˆ 더해주면 [0, 0] μ΄λ”°μœ„λ‘œ 선택

  var sorted = Array.from(numbers).sort((a, b) => a - b),           // sortλŠ” inplace λ©”μ†Œλ“œλΌ deep copy
      argmins = sorted.slice(0, 2).map(e => numbers.indexOf(e));

  return argmins;
}

이제 당신은 νΌλ ˆμ΄λ“œμ˜ κ²Œμž„ ν”Œλ ˆμ΄ μ•Œκ³ λ¦¬μ¦˜μ„ μž‘μ„±ν•  μ€€λΉ„κ°€ λͺ¨λ‘ λλ‚¬μŠ΅λ‹ˆλ‹€. ν–‰μš΄μ„ λΉ•λ‹ˆλ‹€!

πŸ’£ μ£Όμ˜μ‚¬ν•­

ν•¨μˆ˜μ˜ 결과값이 μœ νš¨ν•˜μ§€ μ•Šμ€ 경우(μ˜ˆμ‹œ: play_card()의 결과값이 59) λ˜λŠ” κ³„μ‚°μ‹œκ°„μ΄ 1초λ₯Ό μ΄ˆκ³Όν•˜κ±°λ‚˜ μ§€λ‚˜μΉ˜κ²Œ λ§Žμ€ λ©”λͺ¨λ¦¬λ₯Ό μ‚¬μš©ν•˜λŠ” 경우 κ²½μŸμ—μ„œ μ œμ™Έλ©λ‹ˆλ‹€. μž‘μ„±ν•œ μ•Œκ³ λ¦¬μ¦˜μ€ 본인만 쑰회 λ˜λŠ” μ‚­μ œ κ°€λŠ₯ν•©λ‹ˆλ‹€. λ³΄μ•ˆμƒ 이유둜 eval, Function, setTimeout, setInterval, promise, prototype, document λ“±μ˜ ν‚€μ›Œλ“œ μ‚¬μš©μ„ κΈˆμ§€ν•©λ‹ˆλ‹€.